From 8bc53c1d8f9322b9857a36da274334d9f6d78cec Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 23 Oct 2026 07:51:57 +0700 Subject: [PATCH 01/21] feat(linux): add the DuckDB driver --- linux/ARCHITECTURE.md | 8 +- linux/CONTRIBUTING.md | 2 +- linux/Cargo.lock | 557 +++- linux/Cargo.toml | 13 + linux/README.md | 30 +- linux/ROADMAP.md | 7 +- linux/about.toml | 7 + .../about/bundled/libduckdb-sys/brotli.txt | 19 + .../bundled/libduckdb-sys/concurrentqueue.txt | 61 + .../about/bundled/libduckdb-sys/duckdb.txt | 7 + .../bundled/libduckdb-sys/fast_float.txt | 23 + .../bundled/libduckdb-sys/fastpforlib.txt | 191 ++ .../about/bundled/libduckdb-sys/fmt.txt | 27 + .../about/bundled/libduckdb-sys/fsst.txt | 21 + .../about/bundled/libduckdb-sys/httplib.txt | 21 + .../bundled/libduckdb-sys/hyperloglog.txt | 19 + .../bundled/libduckdb-sys/jaro_winkler.txt | 20 + .../bundled/libduckdb-sys/libpg_query.txt | 28 + .../about/bundled/libduckdb-sys/lz4.txt | 24 + .../about/bundled/libduckdb-sys/manifest.json | 34 + .../about/bundled/libduckdb-sys/mbedtls.txt | 553 ++++ .../about/bundled/libduckdb-sys/miniz.txt | 22 + .../about/bundled/libduckdb-sys/parquet.txt | 217 ++ .../about/bundled/libduckdb-sys/pcg.txt | 19 + .../about/bundled/libduckdb-sys/pdqsort.txt | 16 + .../about/bundled/libduckdb-sys/re2.txt | 27 + .../about/bundled/libduckdb-sys/ska_sort.txt | 3 + .../about/bundled/libduckdb-sys/skiplist.txt | 21 + .../about/bundled/libduckdb-sys/snappy.txt | 54 + .../about/bundled/libduckdb-sys/tdigest.txt | 207 ++ .../about/bundled/libduckdb-sys/thrift.txt | 239 ++ .../about/bundled/libduckdb-sys/utf8proc.txt | 93 + .../about/bundled/libduckdb-sys/vergesort.txt | 21 + .../about/bundled/libduckdb-sys/yyjson.txt | 21 + .../about/bundled/libduckdb-sys/zstd.txt | 30 + linux/build-aux/debian/control | 6 +- linux/build-aux/debian/copyright | 1548 ++++++++++- linux/build-aux/packaging/bundled_licenses.py | 151 + linux/build-aux/packaging/license_metadata.py | 2 +- linux/build-aux/rpm/tablepro.spec | 13 +- .../build-aux/tests/test_bundled_licenses.py | 182 ++ .../build-aux/tests/test_license_metadata.py | 7 + .../build-aux/update-third-party-licenses.sh | 7 + linux/crates/app/Cargo.toml | 1 + .../app/src/application/app_services.rs | 5 +- linux/crates/app/src/lib.rs | 31 + linux/crates/app/src/logging.rs | 7 +- .../crates/app/src/services/file_location.rs | 4 +- .../app/src/services/file_location_kind.rs | 8 +- .../app/src/services/file_resolution.rs | 80 +- .../app/src/services/saved_connection_list.rs | 39 +- .../src/services/saved_connection_object.rs | 47 +- .../services/saved_connection_object_imp.rs | 5 +- ...qlite_file_row.rs => database_file_row.rs} | 128 +- linux/crates/app/src/ui/connect_dialog/imp.rs | 6 +- linux/crates/app/src/ui/connect_dialog/mod.rs | 12 +- .../crates/app/src/ui/connect_dialog/tests.rs | 1 + linux/crates/app/src/ui/error_text/driver.rs | 1 + .../app/src/ui/picker/connection_row.rs | 22 +- linux/crates/app/src/ui/picker/tests.rs | 2 +- ...tion_dialog.rs => file_location_dialog.rs} | 8 +- .../app/src/ui/session_window/locate_file.rs | 46 +- linux/crates/app/src/ui/session_window/mod.rs | 26 +- .../session_window/resolve_file_identity.rs | 81 +- .../session_window/session_window_command.rs | 2 +- .../ui/session_window/session_window_input.rs | 4 +- .../app/src/ui/session_window/tests/mod.rs | 7 + .../src/ui/session_window/tests/open_file.rs | 28 + .../app/src/ui/structure_tab/context.rs | 13 + .../app/src/ui/structure_tab/index_dialog.rs | 54 +- .../crates/app/src/ui/structure_tab/layout.rs | 15 +- linux/crates/core/src/browse/build_export.rs | 13 +- linux/crates/core/src/browse/tests.rs | 27 +- linux/crates/core/src/capabilities.rs | 29 +- linux/crates/core/src/ddl/ddl_capabilities.rs | 3 + linux/crates/core/src/ddl/ddl_dialect.rs | 11 +- linux/crates/core/src/ddl/ddl_operation.rs | 2 + linux/crates/core/src/ddl/table_changes.rs | 8 + linux/crates/core/src/ddl/tests.rs | 1 + linux/crates/core/src/dialect/sql_dialect.rs | 8 + linux/crates/core/src/dialect/test_dialect.rs | 13 + linux/crates/core/src/file_format.rs | 99 + linux/crates/core/src/lib.rs | 2 + linux/crates/core/src/registry.rs | 208 ++ linux/crates/core/src/server_diagnostics.rs | 26 +- linux/crates/core/src/sql_syntax/grammar.rs | 10 +- linux/crates/core/src/sql_syntax/keywords.rs | 29 + .../core/src/sql_syntax/reserved_words.rs | 21 +- .../src/sql_syntax/script/lexical_settings.rs | 2 +- .../src/sql_syntax/script/script_rules.rs | 27 + linux/crates/core/tests/script_corpus.rs | 30 +- linux/crates/drivers/clickhouse/src/ddl.rs | 1 + linux/crates/drivers/duckdb/Cargo.toml | 25 + .../crates/drivers/duckdb/src/capabilities.rs | 35 + .../drivers/duckdb/src/catalog/generated.rs | 101 + .../crates/drivers/duckdb/src/catalog/mod.rs | 601 ++++ linux/crates/drivers/duckdb/src/connection.rs | 481 ++++ .../crates/drivers/duckdb/src/create_file.rs | 230 ++ linux/crates/drivers/duckdb/src/ddl.rs | 412 +++ .../crates/drivers/duckdb/src/decode/bits.rs | 49 + linux/crates/drivers/duckdb/src/decode/mod.rs | 293 ++ .../drivers/duckdb/src/decode/nested.rs | 100 + .../crates/drivers/duckdb/src/decode/text.rs | 261 ++ linux/crates/drivers/duckdb/src/dialect.rs | 449 +++ linux/crates/drivers/duckdb/src/driver.rs | 55 + linux/crates/drivers/duckdb/src/duck_type.rs | 555 ++++ .../drivers/duckdb/src/editor_session.rs | 403 +++ linux/crates/drivers/duckdb/src/encode.rs | 199 ++ linux/crates/drivers/duckdb/src/errors.rs | 207 ++ linux/crates/drivers/duckdb/src/instance.rs | 440 +++ linux/crates/drivers/duckdb/src/lib.rs | 26 + linux/crates/drivers/duckdb/src/pool.rs | 230 ++ .../drivers/duckdb/src/result_stream.rs | 626 +++++ linux/crates/drivers/duckdb/src/stop.rs | 70 + .../drivers/duckdb/src/transaction_state.rs | 31 + .../crates/drivers/duckdb/src/write_batch.rs | 148 + .../duckdb/tests/integration/catalog.rs | 250 ++ .../tests/integration/conformance_engine.rs | 19 + .../drivers/duckdb/tests/integration/ddl.rs | 237 ++ .../duckdb/tests/integration/decode.rs | 124 + .../duckdb/tests/integration/duckdb_engine.rs | 144 + .../drivers/duckdb/tests/integration/main.rs | 26 + .../tests/integration/open_semantics.rs | 340 +++ .../duckdb/tests/integration/round_trip.rs | 11 + .../duckdb/tests/integration/sessions.rs | 282 ++ .../duckdb/tests/integration/support.rs | 152 + .../duckdb/tests/integration/value_corpus.rs | 143 + .../drivers/duckdb/tests/large_table.rs | 12 + linux/crates/drivers/mssql/src/ddl.rs | 1 + linux/crates/drivers/mysql/src/ddl/mod.rs | 1 + linux/crates/drivers/postgres/src/ddl.rs | 1 + .../crates/drivers/sqlite/src/capabilities.rs | 19 +- linux/crates/drivers/sqlite/src/ddl/mod.rs | 1 + .../test-support/src/conformance/cases/ddl.rs | 50 +- .../src/conformance/cases/read_only.rs | 14 + .../src/conformance/cases/suite_flags.rs | 2 +- .../src/conformance/conformance_engine.rs | 5 + linux/crates/test-support/src/fake_driver.rs | 43 +- .../data/app.tablepro.TablePro.desktop.in.in | 4 +- .../app.tablepro.TablePro.metainfo.xml.in.in | 4 +- linux/data/licenses/THIRD-PARTY-NOTICE.md | 2464 +++++++++++++++++ linux/data/licenses/third-party.json | 372 ++- linux/data/resources/resources.gresource.xml | 2 +- linux/data/resources/ui/connect-dialog.ui | 2 +- ...qlite-file-row.ui => database-file-row.ui} | 2 +- linux/data/tablepro.1 | 4 +- linux/deny.toml | 3 +- linux/docs/README.md | 1 + linux/docs/adding-drivers.md | 50 +- .../0009-crypto-provider-and-sources.md | 2 +- linux/docs/decisions/0021-bundled-duckdb.md | 43 + linux/docs/decisions/README.md | 3 +- linux/docs/drivers/duckdb.md | 115 + linux/docs/testing.md | 14 +- linux/po/POTFILES.in | 6 +- 155 files changed, 16472 insertions(+), 354 deletions(-) create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/brotli.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/concurrentqueue.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/duckdb.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/fast_float.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/fastpforlib.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/fmt.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/fsst.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/httplib.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/hyperloglog.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/jaro_winkler.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/libpg_query.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/lz4.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/manifest.json create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/mbedtls.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/miniz.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/parquet.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/pcg.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/pdqsort.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/re2.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/ska_sort.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/skiplist.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/snappy.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/tdigest.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/thrift.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/utf8proc.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/vergesort.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/yyjson.txt create mode 100644 linux/build-aux/about/bundled/libduckdb-sys/zstd.txt create mode 100644 linux/build-aux/packaging/bundled_licenses.py create mode 100644 linux/build-aux/tests/test_bundled_licenses.py rename linux/crates/app/src/ui/connect_dialog/{sqlite_file_row.rs => database_file_row.rs} (65%) rename linux/crates/app/src/ui/session_window/{sqlite_location_dialog.rs => file_location_dialog.rs} (97%) create mode 100644 linux/crates/core/src/file_format.rs create mode 100644 linux/crates/drivers/duckdb/Cargo.toml create mode 100644 linux/crates/drivers/duckdb/src/capabilities.rs create mode 100644 linux/crates/drivers/duckdb/src/catalog/generated.rs create mode 100644 linux/crates/drivers/duckdb/src/catalog/mod.rs create mode 100644 linux/crates/drivers/duckdb/src/connection.rs create mode 100644 linux/crates/drivers/duckdb/src/create_file.rs create mode 100644 linux/crates/drivers/duckdb/src/ddl.rs create mode 100644 linux/crates/drivers/duckdb/src/decode/bits.rs create mode 100644 linux/crates/drivers/duckdb/src/decode/mod.rs create mode 100644 linux/crates/drivers/duckdb/src/decode/nested.rs create mode 100644 linux/crates/drivers/duckdb/src/decode/text.rs create mode 100644 linux/crates/drivers/duckdb/src/dialect.rs create mode 100644 linux/crates/drivers/duckdb/src/driver.rs create mode 100644 linux/crates/drivers/duckdb/src/duck_type.rs create mode 100644 linux/crates/drivers/duckdb/src/editor_session.rs create mode 100644 linux/crates/drivers/duckdb/src/encode.rs create mode 100644 linux/crates/drivers/duckdb/src/errors.rs create mode 100644 linux/crates/drivers/duckdb/src/instance.rs create mode 100644 linux/crates/drivers/duckdb/src/lib.rs create mode 100644 linux/crates/drivers/duckdb/src/pool.rs create mode 100644 linux/crates/drivers/duckdb/src/result_stream.rs create mode 100644 linux/crates/drivers/duckdb/src/stop.rs create mode 100644 linux/crates/drivers/duckdb/src/transaction_state.rs create mode 100644 linux/crates/drivers/duckdb/src/write_batch.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/catalog.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/conformance_engine.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/ddl.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/decode.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/duckdb_engine.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/main.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/open_semantics.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/round_trip.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/sessions.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/support.rs create mode 100644 linux/crates/drivers/duckdb/tests/integration/value_corpus.rs create mode 100644 linux/crates/drivers/duckdb/tests/large_table.rs rename linux/data/resources/ui/{sqlite-file-row.ui => database-file-row.ui} (93%) create mode 100644 linux/docs/decisions/0021-bundled-duckdb.md create mode 100644 linux/docs/drivers/duckdb.md diff --git a/linux/ARCHITECTURE.md b/linux/ARCHITECTURE.md index 3d10d408c3..682e816d69 100644 --- a/linux/ARCHITECTURE.md +++ b/linux/ARCHITECTURE.md @@ -28,6 +28,7 @@ linux/ ├── postgres/ tokio-postgres ├── mysql/ sqlx-mysql, MySQL and MariaDB ├── sqlite/ sqlx-sqlite + ├── duckdb/ duckdb, with DuckDB compiled in ├── mssql/ tiberius └── clickhouse/ clickhouse over hyper ``` @@ -39,7 +40,7 @@ linux/ | `core` | no workspace crate | | `net`, `ssh`, `storage` | `core` | | `drivers/postgres`, `drivers/mysql`, `drivers/mssql`, `drivers/clickhouse` | `core`, `net` | -| `drivers/sqlite` | `core` | +| `drivers/sqlite`, `drivers/duckdb` | `core` | | `session` | `core`, `net`, `ssh`, `storage` | | `app` | every crate above | @@ -66,6 +67,7 @@ The driver registry is built once, by `build_registry` in `crates/app/src/lib.rs fn build_registry() -> DriverRegistry { let mut r = DriverRegistry::new(); r.register(Arc::new(tablepro_driver_clickhouse::ClickhouseDriver)); + r.register(Arc::new(tablepro_driver_duckdb::DuckDbDriver)); r.register(Arc::new(tablepro_driver_mssql::MssqlDriver)); r.register(Arc::new(tablepro_driver_mysql::MysqlDriver)); r.register(Arc::new(tablepro_driver_postgres::PgDriver)); @@ -144,7 +146,7 @@ How the app reaches a database: the contract in `tablepro-core`, a transport tha ### The contract -`DatabaseDriver` describes an engine before any connection exists, through `DriverCapabilities`, and opens a `Connection`. The `Connection` serves the workspace from a pool; every call takes `CallOptions` (a deadline and a cancel token) and runs on a task the connection owns (`OwnedCalls`), so a stop reaches the server instead of leaving the statement running. Saves report what they left in the database through a `WriteLedger`. Each editor tab gets an `EditorSession` of its own that streams results into a `ResultSink`. [docs/adding-drivers.md](docs/adding-drivers.md) is the full contract, [ADR 0016](docs/decisions/0016-driver-contract-v2.md) the reasons for it, and the engine pages say how each driver meets it: [PostgreSQL](docs/drivers/postgresql.md), [MySQL and MariaDB](docs/drivers/mysql.md), [SQLite](docs/drivers/sqlite.md), [SQL Server](docs/drivers/sql-server.md), [ClickHouse](docs/drivers/clickhouse.md). +`DatabaseDriver` describes an engine before any connection exists, through `DriverCapabilities`, and opens a `Connection`. The `Connection` serves the workspace from a pool; every call takes `CallOptions` (a deadline and a cancel token) and runs on a task the connection owns (`OwnedCalls`), so a stop reaches the server instead of leaving the statement running. Saves report what they left in the database through a `WriteLedger`. Each editor tab gets an `EditorSession` of its own that streams results into a `ResultSink`. [docs/adding-drivers.md](docs/adding-drivers.md) is the full contract, [ADR 0016](docs/decisions/0016-driver-contract-v2.md) the reasons for it, and the engine pages say how each driver meets it: [PostgreSQL](docs/drivers/postgresql.md), [MySQL and MariaDB](docs/drivers/mysql.md), [SQLite](docs/drivers/sqlite.md), [DuckDB](docs/drivers/duckdb.md), [SQL Server](docs/drivers/sql-server.md), [ClickHouse](docs/drivers/clickhouse.md). ### SQL scripts @@ -157,6 +159,7 @@ A driver names its grammar through `SqlDialect::grammar`. The rules for each `Sq | PostgreSQL | `;` | dollar-quoted bodies, the `BEGIN ... END` body of a `CREATE FUNCTION` or `PROCEDURE`, parentheses | `E'...'` escapes, nested block comments, backslash escapes only with `standard_conforming_strings` off | | MySQL, MariaDB | `;`, or the delimiter a `DELIMITER` line sets | nothing | backtick names, `"` strings, `#` comments, `--` only before whitespace, a control character or the end of the text, backslash escapes | | SQLite | `;` | a `CREATE TRIGGER` body, as `sqlite3_complete` reads it | `[name]` and backtick names | +| DuckDB | `;` | `$tag$ ... $tag$` strings | `E'...'` escapes, nested block comments | | SQL Server | a `GO` line, with an optional count | the whole batch, whatever `;` it holds | `[name]` with `]]` inside, nested block comments | | ClickHouse | `;` | heredocs, `$tag$ ... $tag$` | backslash escapes in every quote, `//`, `# ` and `#!` comments, nested block comments | @@ -225,6 +228,7 @@ Each engine's TLS stack takes trust in its own form, so the driver builds it: | MySQL, MariaDB | The system store and the CA file as one PEM bundle (`tablepro_net::tls::mysql_ca_bundle_pem`), plus the Mozilla roots compiled into sqlx | Yes | The rustls error | | SQL Server | The system store (loaded by tiberius), plus the CA file | No | rustls's message text, because tiberius keeps nothing else | | SQLite | No TLS | | | +| DuckDB | No TLS | | | PostgreSQL and ClickHouse get their rustls `ClientConfig` from `tablepro_net::tls::build_client_config`. Every engine uses the aws-lc-rs provider ([ADR 0009](docs/decisions/0009-crypto-provider-and-sources.md)). A CA file is added to the engine's roots, never in place of them. `docs/error-handling.md` lists [what each TLS failure means and what the app offers](docs/error-handling.md#tlsfailure-and-its-remedies). diff --git a/linux/CONTRIBUTING.md b/linux/CONTRIBUTING.md index f5af101e19..38926e9bd9 100644 --- a/linux/CONTRIBUTING.md +++ b/linux/CONTRIBUTING.md @@ -113,7 +113,7 @@ This is the most common substantive change. Follow [docs/adding-drivers.md](docs ## Commits -Conventional Commits, single line, same rule as the macOS app. The scope is `linux`, or `linux-` for one area of the app: `linux-grid`, `linux-browse`, `linux-structure`, `linux-editor`, `linux-history`, `linux-export`, `linux-sidebar`, `linux-picker`, `linux-session`, `linux-storage`, `linux-ssh`, `linux-core`, `linux-flatpak`, or a driver as `linux-postgres`, `linux-mysql`, `linux-sqlite`, `linux-mssql`, `linux-clickhouse`. +Conventional Commits, single line, same rule as the macOS app. The scope is `linux`, or `linux-` for one area of the app: `linux-grid`, `linux-browse`, `linux-structure`, `linux-editor`, `linux-history`, `linux-export`, `linux-sidebar`, `linux-picker`, `linux-session`, `linux-storage`, `linux-ssh`, `linux-core`, `linux-flatpak`, or a driver as `linux-postgres`, `linux-mysql`, `linux-sqlite`, `linux-duckdb`, `linux-mssql`, `linux-clickhouse`. ``` fix(linux-sidebar): keep the filter text when the catalog refreshes diff --git a/linux/Cargo.lock b/linux/Cargo.lock index 3f9d81faf2..162017b6e9 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aes" version = "0.8.4" @@ -14,6 +20,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -44,6 +64,169 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arrow" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfdd0833e32a9874d2b55089333ad310c0be208aafa277385ce2461dec90be3" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a41203398f0eaa6f7ec8e62c0da742a21abf282c148fc157f6c35c90e29981a" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae33dad492b7df00a217563a7b0ef2874df68a0deea1b1a3acf628152f7f7a69" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.17.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9552f96391c005e6ab449fa941420935e7e062489b12b8b1b08879b2163f5b5" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8a327c9649f30d8406995f27642b68df354713cca3baaaf100f076f18d5f34" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64 0.22.1", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b24852db04738907e06c04ea61e42fe7fda962a34513022dc0d0e754fb7976b" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a083ec750f5c043f02946b4baf05fcdbb55f4560a3277055caca5cc99f3eb0" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514ba0ef0d4c5896202dae736251ce415abb43a950bed570fb7981b8716c0e4c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ca356ad6425cecb6eb7b28e4f659f1ee7880fbb1a16127de7dd62901efee9e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "arrow-select" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58da39eb3d8350ad4a549e5c2bc49284dac554016c69829310350f1731b0aad" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "58.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6789b388467525e3271326b6b4915666ecfdf5142aef09779445c954b67543c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + [[package]] name = "ashpd" version = "0.13.13" @@ -107,7 +290,7 @@ dependencies = [ "libc", "portable-atomic", "rustc-hash", - "rustix", + "rustix 1.1.4", "tokio", "tokio-stream", "xattr", @@ -546,6 +729,12 @@ dependencies = [ "system-deps 9.0.0", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -592,6 +781,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.2" @@ -712,6 +907,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "connection-string" version = "0.2.0" @@ -730,6 +936,26 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -815,6 +1041,28 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.13.2", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -1106,6 +1354,23 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "duckdb" +version = "1.10506.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a46cc5f64f534993bae616668a94b1b8e58295c2bd39f85afef7c44297817da" +dependencies = [ + "arrow", + "cast", + "comfy-table", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink 0.10.0", + "libduckdb-sys", + "num-integer", + "strum", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1221,6 +1486,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.5.0" @@ -1251,6 +1528,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.12" @@ -1280,6 +1567,8 @@ version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ + "crc32fast", + "miniz_oxide", "zlib-rs", ] @@ -1301,6 +1590,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1522,7 +1817,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-link", ] @@ -1835,6 +2130,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -1844,6 +2140,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1852,7 +2157,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1863,7 +2168,16 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", ] [[package]] @@ -2472,6 +2786,63 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libadwaita" version = "0.9.2" @@ -2508,6 +2879,23 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libduckdb-sys" +version = "1.10506.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113d1c26c9f518405a17b5729bb6ab6abf029fdc46626406922cf9a8dc9b4609" +dependencies = [ + "cc", + "flate2", + "pkg-config", + "serde", + "serde_json", + "tar", + "ureq", + "vcpkg", + "zip 9.0.5", +] + [[package]] name = "libgssapi" version = "0.11.0" @@ -2565,6 +2953,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2676,6 +3070,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.3" @@ -2714,6 +3118,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2826,6 +3242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3144,7 +3561,7 @@ dependencies = [ "base64 0.22.1", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "hmac 0.13.0", "md-5 0.11.0", "memchr", @@ -3161,7 +3578,7 @@ checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" dependencies = [ "bytes", "chrono", - "fallible-iterator", + "fallible-iterator 0.2.0", "postgres-protocol", ] @@ -3498,7 +3915,7 @@ checksum = "34f2d2cda7223168a14fb86bc7411dbc3379bd7d49749ecc53b91ed98585b6b0" dependencies = [ "chrono", "tempfile", - "zip", + "zip 8.6.0", ] [[package]] @@ -3525,6 +3942,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3534,7 +3964,7 @@ dependencies = [ "bitflags 2.13.2", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3547,6 +3977,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4105,7 +4536,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink", + "hashlink 0.11.1", "indexmap 2.14.2", "log", "memchr", @@ -4302,6 +4733,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4392,7 +4844,7 @@ dependencies = [ "log", "proptest", "relm4", - "rustix", + "rustix 1.1.4", "secrecy", "serde", "serde_json", @@ -4400,6 +4852,7 @@ dependencies = [ "sqlformat", "tablepro-core", "tablepro-driver-clickhouse", + "tablepro-driver-duckdb", "tablepro-driver-mssql", "tablepro-driver-mysql", "tablepro-driver-postgres", @@ -4441,7 +4894,7 @@ dependencies = [ "tokio", "tokio-util", "uuid", - "zip", + "zip 8.6.0", ] [[package]] @@ -4472,6 +4925,24 @@ dependencies = [ "uuid", ] +[[package]] +name = "tablepro-driver-duckdb" +version = "0.0.0" +dependencies = [ + "async-trait", + "chrono", + "deadpool", + "duckdb", + "rustix 1.1.4", + "tablepro-core", + "tablepro-test-support", + "tempfile", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "tablepro-driver-mssql" version = "0.0.0" @@ -4522,7 +4993,7 @@ dependencies = [ "bytes", "chrono", "deadpool-postgres", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-util", "postgres-protocol", "postgres-types", @@ -4610,7 +5081,7 @@ version = "0.0.0" dependencies = [ "async-trait", "futures", - "rustix", + "rustix 1.1.4", "secrecy", "tablepro-core", "tablepro-test-fixtures", @@ -4678,6 +5149,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -4697,9 +5179,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4848,6 +5330,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -4923,7 +5414,7 @@ dependencies = [ "async-trait", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-channel", "futures-util", "log", @@ -5250,6 +5741,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -5281,6 +5784,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", + "webpki-roots", ] [[package]] @@ -5722,7 +6226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a8885a854a8bfdf87a301e53e41b17c5f8f33639903131338b997b1eb614f44" dependencies = [ "gethostname", - "rustix", + "rustix 1.1.4", "x11rb-protocol", ] @@ -5769,7 +6273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -5821,7 +6325,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.4", "serde", "serde_repr", "tokio", @@ -5980,6 +6484,21 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zip" +version = "9.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" +dependencies = [ + "crc32fast", + "flate2", + "indexmap 2.14.2", + "memchr", + "nix", + "typed-path", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.7" diff --git a/linux/Cargo.toml b/linux/Cargo.toml index d61ec72a1d..69c08bac40 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/test-fixtures", "crates/test-support", "crates/drivers/clickhouse", + "crates/drivers/duckdb", "crates/drivers/mssql", "crates/drivers/mysql", "crates/drivers/postgres", @@ -25,6 +26,7 @@ publish = false [workspace.dependencies] tablepro-core = { path = "crates/core" } tablepro-driver-clickhouse = { path = "crates/drivers/clickhouse" } +tablepro-driver-duckdb = { path = "crates/drivers/duckdb" } tablepro-driver-mssql = { path = "crates/drivers/mssql" } tablepro-driver-mysql = { path = "crates/drivers/mysql" } tablepro-driver-postgres = { path = "crates/drivers/postgres" } @@ -72,6 +74,10 @@ clickhouse-types = "0.1.3" # provider feature is needed, and leaving both off keeps ring out. tokio-postgres-rustls = { version = "0.14.0", default-features = false } libsqlite3-sys = ">=0.30.1, <0.38.0" +# DuckDB 1.5, compiled from the sources the crate ships, since only Ubuntu +# 26.10 packages libduckdb (docs/decisions/0021-bundled-duckdb.md). json and +# parquet are linked in, since extensions are never downloaded. +duckdb = { version = "~1.10506", default-features = false, features = ["bundled", "json", "parquet"] } num-bigint = { version = "0.4", default-features = false, features = ["std"] } proptest = "1.11" rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std", "tls12"] } @@ -120,6 +126,13 @@ ignore_without_reason = "deny" too_many_lines = "deny" self_named_module_files = "deny" +# Unoptimised with debug info, DuckDB's archive is 2.6 GB, which every +# test binary of the app links, and the engine runs the suites at debug +# speed. +[profile.dev.package.libduckdb-sys] +opt-level = 2 +debug = false + [profile.release] lto = "fat" codegen-units = 1 diff --git a/linux/README.md b/linux/README.md index 055dd260fa..a08faeb4be 100644 --- a/linux/README.md +++ b/linux/README.md @@ -4,7 +4,7 @@ Native Linux database client, the Linux sibling of the macOS TablePro app. The t ## Status -It connects to five engines: PostgreSQL, MySQL and MariaDB, SQL Server, ClickHouse and SQLite. Each connection session gets a window of its own. Nothing is released yet: no `linux-v*` tag exists. [ROADMAP.md](ROADMAP.md) lists what is built, what is not, and the release criteria. +It connects to six engines: PostgreSQL, MySQL and MariaDB, SQL Server, ClickHouse, SQLite and DuckDB. Each connection session gets a window of its own. Nothing is released yet: no `linux-v*` tag exists. [ROADMAP.md](ROADMAP.md) lists what is built, what is not, and the release criteria. ## Supported servers @@ -18,6 +18,7 @@ TablePro supports the server releases their vendors still support. CI runs each | SQL Server | 2017, 2019, 2022 and 2025 | 2016 and older, past extended support (2016 on 2026-07-14) | | ClickHouse | 26.3 and 26.8 | Monthly releases between long-term ones. 25.8 and older long-term releases, past end of life (25.8 on 2026-08-29) | | SQLite | the system library, 3.37.0 or newer | older libraries, which the driver refuses when it opens a file | +| DuckDB | 1.5.6, compiled into the app ([ADR 0021](docs/decisions/0021-bundled-duckdb.md)) | files a newer DuckDB wrote in a storage format 1.5.6 cannot read | - PostgreSQL 14 reaches end of life on 2026-11-12. - MySQL 8.0 reached end of life on 2026-04-30, but Amazon RDS keeps 8.0 instances under paid Extended Support until 2029-07-31, so it stays tested. @@ -47,7 +48,7 @@ apt and dnf, and Software Updater and GNOME Software with them, install the upda ## Privacy -TablePro makes no network requests of its own: no telemetry, no update check and no crash upload. It connects to the database servers and SSH hosts you set up, and nothing else. Crash reports stay on your disk until you attach one to a bug report. Updates come from apt or dnf. +TablePro makes no network requests of its own: no telemetry, no update check and no crash upload. DuckDB fetches no extension unless your own SQL runs `INSTALL`. It connects to the database servers and SSH hosts you set up, and nothing else. Crash reports stay on your disk until you attach one to a bug report. Updates come from apt or dnf. ## Stack @@ -57,7 +58,7 @@ TablePro makes no network requests of its own: no telemetry, no update check and | GUI toolkit | GTK 4, libadwaita, GtkSourceView and GLib at the GNOME 50 floor ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor.md)) | | App architecture | gtk-rs subclasses with composite templates, one window per connection session ([ADR 0012](docs/decisions/0012-window-per-session.md)); the saved-queries and CSV import dialogs are still [Relm4](https://relm4.org) 0.11 components | | Async | the app's tokio runtime (`AppRuntime`) for background work, results delivered to the GLib main loop | -| Database drivers | tokio-postgres with deadpool-postgres (PostgreSQL), sqlx 0.9 (MySQL and MariaDB, SQLite), tiberius 0.13 (SQL Server) and the official `clickhouse` crate, all from crates.io ([ADR 0016](docs/decisions/0016-driver-contract-v2.md)) | +| Database drivers | tokio-postgres with deadpool-postgres (PostgreSQL), sqlx 0.9 (MySQL and MariaDB, SQLite), tiberius 0.13 (SQL Server), the official `clickhouse` crate and the `duckdb` crate with DuckDB compiled in, all from crates.io ([ADR 0016](docs/decisions/0016-driver-contract-v2.md)) | | SSH | the system OpenSSH client ([ADR 0013](docs/decisions/0013-openssh-client-transport.md)) | | TLS | rustls with aws-lc-rs as the only crypto provider ([ADR 0009](docs/decisions/0009-crypto-provider-and-sources.md)) | | Secrets | oo7: the Secret portal inside Flatpak, the Secret Service over D-Bus on the host ([ADR 0004](docs/decisions/0004-libsecret-secret-storage.md)) | @@ -83,7 +84,7 @@ System dependencies: sudo apt install -y build-essential pkgconf meson ninja-build gettext desktop-file-utils appstream libglib2.0-bin libxml2-utils libgtk-4-dev libadwaita-1-dev libgtksourceview-5-dev libsqlite3-dev libkrb5-dev clang libclang-dev openssh-client # Fedora 44 -sudo dnf install -y gcc pkgconf meson ninja-build gettext desktop-file-utils appstream glib2-devel libxml2 gtk4-devel libadwaita-devel gtksourceview5-devel sqlite-devel krb5-devel clang-devel openssh-clients +sudo dnf install -y gcc gcc-c++ pkgconf meson ninja-build gettext desktop-file-utils appstream glib2-devel libxml2 gtk4-devel libadwaita-devel gtksourceview5-devel sqlite-devel krb5-devel clang-devel openssh-clients # Arch (not covered by CI) sudo pacman -S --needed base-devel pkgconf meson ninja gettext desktop-file-utils appstream libxml2 gtk4 libadwaita gtksourceview5 sqlite krb5 clang openssh @@ -142,6 +143,10 @@ link the system SQLite through sqlx's `sqlite-unbundled` feature rather than compiling a second copy into the binary. `libclang-dev` lets `libsqlite3-sys` run bindgen against the installed `sqlite3.h`. +A C++ compiler is there for DuckDB, which the `duckdb` crate compiles +from its own sources: `build-essential` brings g++ on Ubuntu, `gcc-c++` +on Fedora. The first build of it takes several minutes. + `libkrb5-dev` is there for the app's default `kerberos` feature: SQL Server Windows integrated auth links MIT Kerberos and runs bindgen against its headers. `meson setup _build -Dkerberos=false`, or @@ -222,19 +227,22 @@ After a change to `Cargo.lock`, `build-aux/update-third-party-licenses.sh` rewrites `debian/copyright` and the spec's `License` tag along with `data/licenses`. -## Opening SQLite files +## Opening database files Once installed, TablePro is in the file manager's **Open With** for -SQLite databases (`application/vnd.sqlite3`). From a terminal: +SQLite databases (`application/vnd.sqlite3`). shared-mime-info has no +type for DuckDB files, so a `.duckdb` file opens from the connect dialog +or a terminal: ```bash -tablepro app.sqlite3 +tablepro app.sqlite3 sales.duckdb ``` -A running TablePro opens the file in its own window and the new launch -exits. A file already open brings its window forward. A database on a -network or FUSE filesystem asks whether to open read-only first, because -SQLite's locking cannot be trusted there. +The file's own header picks the engine, whatever its name. A running +TablePro opens each file in its own window and the new launch exits. A +file already open brings its window forward. A database on a network or +FUSE filesystem asks whether to open read-only first, because neither +engine's locking can be trusted there. ## SQL Server with Windows integrated auth diff --git a/linux/ROADMAP.md b/linux/ROADMAP.md index 4252135c22..2471cb2f5f 100644 --- a/linux/ROADMAP.md +++ b/linux/ROADMAP.md @@ -2,7 +2,7 @@ **Status, 2026-10-19**: every capability in the tables below is in the tree. Nothing has shipped yet: no `linux-v*` tag has been cut. The [release criteria](#release-criteria) say what has to hold first, and the [backlog](#backlog) lists what is not built. -The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor.md)) and connects to five engines, each a crate linked in at compile time ([ADR 0001](docs/decisions/0001-no-plugin-system.md)): [PostgreSQL](docs/drivers/postgresql.md), [MySQL and MariaDB](docs/drivers/mysql.md), [SQL Server](docs/drivers/sql-server.md), [ClickHouse](docs/drivers/clickhouse.md) and [SQLite](docs/drivers/sqlite.md). +The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor.md)) and connects to six engines, each a crate linked in at compile time ([ADR 0001](docs/decisions/0001-no-plugin-system.md)): [PostgreSQL](docs/drivers/postgresql.md), [MySQL and MariaDB](docs/drivers/mysql.md), [SQL Server](docs/drivers/sql-server.md), [ClickHouse](docs/drivers/clickhouse.md), [SQLite](docs/drivers/sqlite.md) and [DuckDB](docs/drivers/duckdb.md). ## Implemented @@ -15,7 +15,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor | Import and export | A JSON file with no secrets in it. On import, an entry with the same id and the same settings updates its name, colour and group; any other is added as a new connection | | Test before saving | Test connects once and reports, then disconnects | | Passwords | Kept in the Secret Service; a prompt asks when one is missing or refused, with Remember Password | -| Safe Mode | A level per connection, set in the connect dialog or the main menu while connected and saved back to the connection. Off asks before DROP, TRUNCATE, and DELETE or UPDATE without WHERE. Confirm Writes also asks before every other write from the editor, a grid save, a structure save, a CSV import or Drop Table, showing the SQL and the connection, with Cancel the default. Read-Only refuses every editor statement that does not read, and opens the connection read-only: SQLite and ClickHouse refuse writes at the engine, PostgreSQL and MySQL run in read-only transactions, and SQL Server relies on the login, with the app's own reads in a rolled-back transaction. A statement is a read only when its engine's grammar shows it reads, each statement of a script on its own. Moving to or from Read-Only reconnects the window | +| Safe Mode | A level per connection, set in the connect dialog or the main menu while connected and saved back to the connection. Off asks before DROP, TRUNCATE, and DELETE or UPDATE without WHERE. Confirm Writes also asks before every other write from the editor, a grid save, a structure save, a CSV import or Drop Table, showing the SQL and the connection, with Cancel the default. Read-Only refuses every editor statement that does not read, and opens the connection read-only: SQLite, DuckDB and ClickHouse refuse writes at the engine, PostgreSQL and MySQL run in read-only transactions, and SQL Server relies on the login, with the app's own reads in a rolled-back transaction. A statement is a read only when its engine's grammar shows it reads, each statement of a script on its own. Moving to or from Read-Only reconnects the window | | SQLite files | Choose a file, create a new one, open one from Files, find a moved one with Locate, and choose read-only or read-write for one on a network or FUSE filesystem | | Kerberos for SQL Server | From the ticket cache, in the Flatpak through the KCM socket | | Health and reconnect | A banner while the connection is lost, reconnect with a backoff from 5 to 60 seconds, and metadata, page and count reads replayed once after it; writes are never replayed | @@ -162,7 +162,6 @@ Each one is a crate under `crates/drivers` ([adding-drivers.md](docs/adding-driv |---|---|---| | Redis | redis 1.x (redis-rs) | None. fred, named here before, has had no release since 2025-02-27 | | MongoDB | mongodb 3.x, default features off, `rustls-tls-aws-lc` | It must use the system trust store first: it fills its roots from webpki-roots unless `ca_file_path` is set, so it needs native roots upstream or `ca_file_path` set to the platform bundle | -| DuckDB | duckdb, from the DuckDB project | None | | Cassandra and ScyllaDB | scylla 1.x with `rustls-023` | None | | Oracle | none | Blocked. The oracle crate has one maintainer, no release since 2025-01-02, and needs the proprietary Oracle client | @@ -181,7 +180,7 @@ What has to hold before the first release, for a user on GNOME 50 with Ubuntu 26 | 1 | Installs `tablepro` from the PPA or the COPR project, and gets updates from the system's updater | CI builds both packages offline the way Launchpad and COPR do, and starts them in fresh containers | The release setup, the first tag, and an install then an upgrade from the PPA and COPR | | 2 | Connects to their everyday PostgreSQL, MySQL, MariaDB, SQL Server, ClickHouse or SQLite database | The supported releases and why others are out, in the README's [supported servers](README.md#supported-servers). CI runs every driver's Docker suite on each tested release, MySQL 8.0 and SQL Server 2017 and 2019 among them | Nothing | | 3 | Browses tables of real-world data (dates, decimals, JSON, UUIDs, NULLs) and sees correct values | Per-engine decoding with the type fallback and unsupported-type conformance cases ([ADR 0014](docs/decisions/0014-lossless-values-and-dialects.md)). A value corpus on every engine, MariaDB too: dates and times at their extremes and in other zones, decimals, JSON, UUIDs, Unicode and binary values, read back through a table tab, Export All and an editor run, and exported as CSV and SQL INSERT and loaded back into an empty copy | Nothing | -| 4 | Runs typical queries against tables of a million rows or more without running out of memory or freezing | The 10,000-row editor limit with Export All, and driver tests over 2,000,000 rows. Pages seek past the edge row's key, or walk PostgreSQL heap block windows for a table with no key; OFFSET only under a user sort, a restored deep offset or a partitioned keyless table. Docker tests page 60k and 25k-row tables both ways and check the plan. A 1,000,000-row table is paged to its end and exported on PostgreSQL, MySQL, MariaDB, SQL Server, ClickHouse and SQLite, with bounds on time and on resident peak growth. SQLite's case runs in the unit suite in a test binary of its own, so the peak is its alone | Nothing | +| 4 | Runs typical queries against tables of a million rows or more without running out of memory or freezing | The 10,000-row editor limit with Export All, and driver tests over 2,000,000 rows. Pages seek past the edge row's key, or walk PostgreSQL heap block windows for a table with no key; OFFSET only under a user sort, a restored deep offset or a partitioned keyless table. Docker tests page 60k and 25k-row tables both ways and check the plan. A 1,000,000-row table is paged to its end and exported on PostgreSQL, MySQL, MariaDB, SQL Server, ClickHouse, SQLite and DuckDB, with bounds on time and on resident peak growth. The file engines' cases run in the unit suite, each in a test binary of its own, so the peak is its alone | Nothing | | 5 | Edits data in place, and the changes commit correctly even when the connection or the app misbehaves | Write, row guard and commit resolution conformance on every engine, with the fault fakes and the grid save Docker test. `kill_during_save` kills TablePro halfway through a SQLite save: the file keeps none of it, and the next launch restores the session with its history, settings and saved tabs. A save into a SQLite file another program holds locked fails as `Busy(FileLocked)`, which says another program or an editor tab holds the file's lock, and leaves the file as it was whether the lock shows at `BEGIN IMMEDIATE` or at the `COMMIT` | Nothing | | 6 | Trusts the app with credentials: TLS, SSH tunnels, no password in plain text | Verify full by default, the TLS conformance suite, the Secret Service, and statement logging silenced | An outside security review | | 7 | Recovers when the network drops or the database restarts | Health monitoring, backoff and replay rules with their tests, a stopped container in the liveness tests, and a session test that restarts PostgreSQL: the read in flight runs again, the save in flight does not and leaves no row | Nothing | diff --git a/linux/about.toml b/linux/about.toml index b3a39bafca..c23f7318e5 100644 --- a/linux/about.toml +++ b/linux/about.toml @@ -155,3 +155,10 @@ license = "MIT OR Apache-2.0" path = "LICENSE-MIT" license = "MIT" checksum = "5abbdd842310c8f1650fcde3a5a51a3e09412b9e5a6eea87519e0db9e32b339d" + +# The crate packages no license file; the repository root holds it. +[duckdb.clarify] +license = "MIT" +[[duckdb.clarify.git]] +path = "LICENSE" +checksum = "e00a318e49c9ff90254194864b2b695efc04c0384a8682c93ef51518e77ff5ea" diff --git a/linux/build-aux/about/bundled/libduckdb-sys/brotli.txt b/linux/build-aux/about/bundled/libduckdb-sys/brotli.txt new file mode 100644 index 0000000000..33b7cdd2db --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/brotli.txt @@ -0,0 +1,19 @@ +Copyright (c) 2009, 2010, 2013-2016 by the Brotli 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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/concurrentqueue.txt b/linux/build-aux/about/bundled/libduckdb-sys/concurrentqueue.txt new file mode 100644 index 0000000000..47efd43952 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/concurrentqueue.txt @@ -0,0 +1,61 @@ +This license file applies to everything in this repository except that which +is explicitly annotated as being written by other authors, i.e. the Boost +queue (included in the benchmarks for comparison), Intel's TBB library (ditto), +the CDSChecker tool (used for verification), the Relacy model checker (ditto), +and Jeff Preshing's semaphore implementation (used in the blocking queue) which +has a zlib license (embedded in lightweightsempahore.h). + +--- + +Simplified BSD License: + +Copyright (c) 2013-2016, Cameron Desrochers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of +conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this list of +conditions and the following disclaimer in the documentation and/or other materials +provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +I have also chosen to dual-license under the Boost Software License as an alternative to +the Simplified BSD license above: + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/duckdb.txt b/linux/build-aux/about/bundled/libduckdb-sys/duckdb.txt new file mode 100644 index 0000000000..4e1fbb7695 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/duckdb.txt @@ -0,0 +1,7 @@ +Copyright 2018-2025 Stichting DuckDB Foundation + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/fast_float.txt b/linux/build-aux/about/bundled/libduckdb-sys/fast_float.txt new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/fast_float.txt @@ -0,0 +1,23 @@ +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/fastpforlib.txt b/linux/build-aux/about/bundled/libduckdb-sys/fastpforlib.txt new file mode 100644 index 0000000000..37ec93a14f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/fastpforlib.txt @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/fmt.txt b/linux/build-aux/about/bundled/libduckdb-sys/fmt.txt new file mode 100644 index 0000000000..f0ec3db4d2 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/fmt.txt @@ -0,0 +1,27 @@ +Copyright (c) 2012 - present, Victor Zverovich + +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. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/fsst.txt b/linux/build-aux/about/bundled/libduckdb-sys/fsst.txt new file mode 100644 index 0000000000..edb46d2db5 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/fsst.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2020, CWI, TU Munich, FSU Jena + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/httplib.txt b/linux/build-aux/about/bundled/libduckdb-sys/httplib.txt new file mode 100644 index 0000000000..47c418e072 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/httplib.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/hyperloglog.txt b/linux/build-aux/about/bundled/libduckdb-sys/hyperloglog.txt new file mode 100644 index 0000000000..5c4921b1e6 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/hyperloglog.txt @@ -0,0 +1,19 @@ +Copyright (c) 2012 Art.sy, Inc. + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/jaro_winkler.txt b/linux/build-aux/about/bundled/libduckdb-sys/jaro_winkler.txt new file mode 100644 index 0000000000..945ce9fa1f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/jaro_winkler.txt @@ -0,0 +1,20 @@ +Copyright © 2022 Max Bachmann + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/libpg_query.txt b/linux/build-aux/about/bundled/libduckdb-sys/libpg_query.txt new file mode 100644 index 0000000000..feacffca4f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/libpg_query.txt @@ -0,0 +1,28 @@ +Copyright (c) 2015, Lukas Fittl +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +* Neither the name of pg_query nor the names of its contributors may be used +to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/lz4.txt b/linux/build-aux/about/bundled/libduckdb-sys/lz4.txt new file mode 100644 index 0000000000..488491695a --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/lz4.txt @@ -0,0 +1,24 @@ +LZ4 Library +Copyright (c) 2011-2020, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/manifest.json b/linux/build-aux/about/bundled/libduckdb-sys/manifest.json new file mode 100644 index 0000000000..b005896171 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/manifest.json @@ -0,0 +1,34 @@ +{ + "crate": "libduckdb-sys", + "version": "1.10506.1", + "source": "https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef", + "components": [ + { "name": "DuckDB", "path": "LICENSE", "file": "duckdb.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "Brotli", "path": "third_party/brotli/LICENSE", "file": "brotli.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "concurrentqueue", "path": "third_party/concurrentqueue/LICENSE", "file": "concurrentqueue.txt", "id": "BSD-2-Clause", "license": "(BSD-2-Clause OR BSL-1.0) AND Zlib", "license_name": "BSD 2-Clause \"Simplified\" License" }, + { "name": "fast_float", "path": "third_party/fast_float/LICENSE", "file": "fast_float.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "FastPFor", "path": "third_party/fastpforlib/LICENSE", "file": "fastpforlib.txt", "id": "Apache-2.0", "license": "Apache-2.0", "license_name": "Apache License 2.0" }, + { "name": "fmt", "path": "third_party/fmt/LICENSE", "file": "fmt.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "FSST", "path": "third_party/fsst/LICENSE", "file": "fsst.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "cpp-httplib", "path": "third_party/httplib/LICENSE", "file": "httplib.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "HyperLogLog", "path": "third_party/hyperloglog/LICENSE", "file": "hyperloglog.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "jaro_winkler", "path": "third_party/jaro_winkler/LICENSE", "file": "jaro_winkler.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "libpg_query", "path": "third_party/libpg_query/LICENSE", "file": "libpg_query.txt", "id": "BSD-3-Clause", "license": "BSD-3-Clause", "license_name": "BSD 3-Clause \"New\" or \"Revised\" License" }, + { "name": "LZ4", "path": "third_party/lz4/LICENSE", "file": "lz4.txt", "id": "BSD-2-Clause", "license": "BSD-2-Clause", "license_name": "BSD 2-Clause \"Simplified\" License" }, + { "name": "Mbed TLS", "path": "third_party/mbedtls/LICENSE", "file": "mbedtls.txt", "id": "Apache-2.0", "license": "Apache-2.0 OR GPL-2.0-or-later", "license_name": "Apache License 2.0" }, + { "name": "miniz", "path": "third_party/miniz/LICENSE", "file": "miniz.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "Apache Parquet", "path": "third_party/parquet/LICENSE", "file": "parquet.txt", "id": "Apache-2.0", "license": "Apache-2.0", "license_name": "Apache License 2.0" }, + { "name": "PCG", "path": "third_party/pcg/LICENSE", "file": "pcg.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "pdqsort", "path": "third_party/pdqsort/LICENSE", "file": "pdqsort.txt", "id": "Zlib", "license": "Zlib", "license_name": "zlib License" }, + { "name": "RE2", "path": "third_party/re2/LICENSE", "file": "re2.txt", "id": "BSD-3-Clause", "license": "BSD-3-Clause", "license_name": "BSD 3-Clause \"New\" or \"Revised\" License" }, + { "name": "ska_sort", "path": "third_party/ska_sort/LICENSE", "file": "ska_sort.txt", "id": "BSL-1.0", "license": "BSL-1.0", "license_name": "Boost Software License 1.0" }, + { "name": "skiplist", "path": "third_party/skiplist/LICENSE", "file": "skiplist.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "Snappy", "path": "third_party/snappy/LICENSE", "file": "snappy.txt", "id": "BSD-3-Clause", "license": "BSD-3-Clause", "license_name": "BSD 3-Clause \"New\" or \"Revised\" License" }, + { "name": "t-digest", "path": "third_party/tdigest/LICENSE and NOTICES", "file": "tdigest.txt", "id": "Apache-2.0", "license": "Apache-2.0", "license_name": "Apache License 2.0" }, + { "name": "Apache Thrift", "path": "third_party/thrift/thrift/LICENSE", "file": "thrift.txt", "id": "Apache-2.0", "license": "Apache-2.0", "license_name": "Apache License 2.0" }, + { "name": "utf8proc", "path": "third_party/utf8proc/LICENSE", "file": "utf8proc.txt", "id": "Unicode-DFS-2015", "license": "MIT AND Unicode-DFS-2015", "license_name": "Unicode License Agreement - Data Files and Software (2015)" }, + { "name": "vergesort", "path": "third_party/vergesort/LICENSE", "file": "vergesort.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "yyjson", "path": "third_party/yyjson/LICENSE", "file": "yyjson.txt", "id": "MIT", "license": "MIT", "license_name": "MIT License" }, + { "name": "Zstandard", "path": "third_party/zstd/LICENSE", "file": "zstd.txt", "id": "BSD-3-Clause", "license": "BSD-3-Clause", "license_name": "BSD 3-Clause \"New\" or \"Revised\" License" } + ] +} diff --git a/linux/build-aux/about/bundled/libduckdb-sys/mbedtls.txt b/linux/build-aux/about/bundled/libduckdb-sys/mbedtls.txt new file mode 100644 index 0000000000..776ac77eaf --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/mbedtls.txt @@ -0,0 +1,553 @@ +Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) +OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license. +This means that users may choose which of these licenses they take the code +under. + +The full text of each of these licenses is given below. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +=============================================================================== + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/miniz.txt b/linux/build-aux/about/bundled/libduckdb-sys/miniz.txt new file mode 100644 index 0000000000..b6ff45a30f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/miniz.txt @@ -0,0 +1,22 @@ +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/parquet.txt b/linux/build-aux/about/bundled/libduckdb-sys/parquet.txt new file mode 100644 index 0000000000..09c6877672 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/parquet.txt @@ -0,0 +1,217 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +-------------------------------------------------------------------------------- + +This product includes code from Apache Avro. + +Copyright: 2014 The Apache Software Foundation. +Home page: https://avro.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Daniel Lemire's JavaFastPFOR project. The +"Lemire" bit packing source code produced by parquet-generator is derived from +the JavaFastPFOR project. + +Copyright: 2013 Daniel Lemire +Home page: http://lemire.me/en/ +Project page: https://github.com/lemire/JavaFastPFOR +License: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This product includes code from Apache Spark. + +* dev/merge_parquet_pr.py is based on Spark's dev/merge_spark_pr.py + +Copyright: 2014 The Apache Software Foundation. +Home page: https://spark.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This product includes code from Twitter's ElephantBird project. + +* parquet-hadoop's UnmaterializableRecordCounter.java includes code from + ElephantBird's LzoRecordReader.java + +Copyright: 2012-2014 Twitter +Home page: https://github.com/twitter/elephant-bird +License: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/linux/build-aux/about/bundled/libduckdb-sys/pcg.txt b/linux/build-aux/about/bundled/libduckdb-sys/pcg.txt new file mode 100644 index 0000000000..2315960363 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/pcg.txt @@ -0,0 +1,19 @@ +Copyright (c) 2014-2017 Melissa O'Neill and PCG Project 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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/pdqsort.txt b/linux/build-aux/about/bundled/libduckdb-sys/pdqsort.txt new file mode 100644 index 0000000000..41d496076f --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/pdqsort.txt @@ -0,0 +1,16 @@ +Copyright (c) 2021 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In no event will the +authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as + being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/re2.txt b/linux/build-aux/about/bundled/libduckdb-sys/re2.txt new file mode 100644 index 0000000000..09e5ec1c74 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/re2.txt @@ -0,0 +1,27 @@ +// Copyright (c) 2009 The RE2 Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/ska_sort.txt b/linux/build-aux/about/bundled/libduckdb-sys/ska_sort.txt new file mode 100644 index 0000000000..0f59862c6b --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/ska_sort.txt @@ -0,0 +1,3 @@ + Copyright Malte Skarupke 2016. + Distributed under the Boost Software License, Version 1.0. + (See http://www.boost.org/LICENSE_1_0.txt) diff --git a/linux/build-aux/about/bundled/libduckdb-sys/skiplist.txt b/linux/build-aux/about/bundled/libduckdb-sys/skiplist.txt new file mode 100644 index 0000000000..cee9bac95d --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/skiplist.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2023 Paul Ross + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/snappy.txt b/linux/build-aux/about/bundled/libduckdb-sys/snappy.txt new file mode 100644 index 0000000000..bd0e5971db --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/snappy.txt @@ -0,0 +1,54 @@ +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in testdata/ is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). diff --git a/linux/build-aux/about/bundled/libduckdb-sys/tdigest.txt b/linux/build-aux/about/bundled/libduckdb-sys/tdigest.txt new file mode 100644 index 0000000000..b8fcf4a689 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/tdigest.txt @@ -0,0 +1,207 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +The Java version of the t-digest was originally authored by Ted Dunning + +A number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) to the Java version. + +The C++ version herein is a derivative of the Java version. It was written by Derrick R. Burns (https:://github.com/derrickburns). +The main modifications are 1) higher performance multi- t-digest merging and 2) faster quantile() and cdf() computation. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/thrift.txt b/linux/build-aux/about/bundled/libduckdb-sys/thrift.txt new file mode 100644 index 0000000000..3b6d7d74cc --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/thrift.txt @@ -0,0 +1,239 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------- +SOFTWARE DISTRIBUTED WITH THRIFT: + +The Apache Thrift software includes a number of subcomponents with +separate copyright notices and license terms. Your use of the source +code for the these subcomponents is subject to the terms and +conditions of the following licenses. + +-------------------------------------------------- +Portions of the following files are licensed under the MIT License: + + lib/erl/src/Makefile.am + +Please see doc/otp-base-license.txt for the full terms of this license. + +-------------------------------------------------- +For the aclocal/ax_boost_base.m4 and contrib/fb303/aclocal/ax_boost_base.m4 components: + +# Copyright (c) 2007 Thomas Porschberg +# +# Copying and distribution of this file, with or without +# modification, are permitted in any medium without royalty provided +# the copyright notice and this notice are preserved. + +-------------------------------------------------- +For the lib/nodejs/lib/thrift/json_parse.js: + +/* + json_parse.js + 2015-05-02 + Public Domain. + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +*/ +(By Douglas Crockford ) +-------------------------------------------------- diff --git a/linux/build-aux/about/bundled/libduckdb-sys/utf8proc.txt b/linux/build-aux/about/bundled/libduckdb-sys/utf8proc.txt new file mode 100644 index 0000000000..80fab6ee6e --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/utf8proc.txt @@ -0,0 +1,93 @@ +## utf8proc license ## + +**utf8proc** is a software package originally developed +by Jan Behrens and the rest of the Public Software Group, who +deserve nearly all of the credit for this library, that is now maintained by the Julia-language developers. Like the original utf8proc, +whose copyright and license statements are reproduced below, all new +work on the utf8proc library is licensed under the [MIT "expat" +license](http://opensource.org/licenses/MIT): + +*Copyright © 2014-2019 by Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas Fonseca, and other contributors listed in the git history.* + +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. + +## Original utf8proc license ## + +*Copyright (c) 2009, 2013 Public Software Group e. V., Berlin, Germany* + +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. + +## Unicode data license ## + +This software contains data (`utf8proc_data.c`) derived from processing +the Unicode data files. The following license applies to that data: + +**COPYRIGHT AND PERMISSION NOTICE** + +*Copyright (c) 1991-2007 Unicode, Inc. All rights reserved. Distributed +under the Terms of Use in http://www.unicode.org/copyright.html.* + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Unicode data files and any associated documentation (the "Data +Files") or Unicode software and any associated documentation (the +"Software") to deal in the Data Files or Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, and/or sell copies of the Data Files or Software, and +to permit persons to whom the Data Files or Software are furnished to do +so, provided that (a) the above copyright notice(s) and this permission +notice appear with all copies of the Data Files or Software, (b) both the +above copyright notice(s) and this permission notice appear in associated +documentation, and (c) there is clear notice in each modified Data File or +in the Software as well as in the documentation associated with the Data +File(s) or Software that the data or software has been modified. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS +INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL 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 THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be +registered in some jurisdictions. All other trademarks and registered +trademarks mentioned herein are the property of their respective owners. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/vergesort.txt b/linux/build-aux/about/bundled/libduckdb-sys/vergesort.txt new file mode 100644 index 0000000000..12bbbf2197 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/vergesort.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Morwenn + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/yyjson.txt b/linux/build-aux/about/bundled/libduckdb-sys/yyjson.txt new file mode 100644 index 0000000000..a09bff3553 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/yyjson.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 YaoYuan + +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. diff --git a/linux/build-aux/about/bundled/libduckdb-sys/zstd.txt b/linux/build-aux/about/bundled/libduckdb-sys/zstd.txt new file mode 100644 index 0000000000..a793a80289 --- /dev/null +++ b/linux/build-aux/about/bundled/libduckdb-sys/zstd.txt @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/linux/build-aux/debian/control b/linux/build-aux/debian/control index 8c6e964b70..dbc5b2a12f 100644 --- a/linux/build-aux/debian/control +++ b/linux/build-aux/debian/control @@ -45,6 +45,6 @@ Recommends: Suggests: krb5-user, Description: database client for GNOME - TablePro connects to PostgreSQL, MySQL, SQLite, SQL Server and ClickHouse. - Browse tables, edit rows in place, run SQL with syntax highlighting, and - reach servers behind an SSH bastion. + TablePro connects to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server and + ClickHouse. Browse tables, edit rows in place, run SQL with syntax + highlighting, and reach servers behind an SSH bastion. diff --git a/linux/build-aux/debian/copyright b/linux/build-aux/debian/copyright index 68c51d7b12..2f05887549 100644 --- a/linux/build-aux/debian/copyright +++ b/linux/build-aux/debian/copyright @@ -708,6 +708,35 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/ahash-0.8.12/* +Copyright: 2018 Tom Kaitchuck +License: Apache-2.0 or Expat + Copyright (c) 2018 Tom Kaitchuck + . + 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. + Files: vendor/aho-corasick-1.1.5/* vendor/byteorder-1.5.0/* vendor/csv-1.4.0/* @@ -764,6 +793,103 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/arrow-58.4.0/* + vendor/arrow-arith-58.4.0/* + vendor/arrow-buffer-58.4.0/* + vendor/arrow-cast-58.4.0/* + vendor/arrow-data-58.4.0/* + vendor/arrow-ord-58.4.0/* + vendor/arrow-row-58.4.0/* + vendor/arrow-schema-58.4.0/* + vendor/arrow-select-58.4.0/* + vendor/arrow-string-58.4.0/* + vendor/flagset-0.4.7/* + vendor/fragile-2.1.0/* +Copyright: the authors of arrow, arrow-arith, arrow-buffer, arrow-cast, arrow-data, + arrow-ord, arrow-row, arrow-schema, arrow-select, arrow-string, flagset, + fragile +License: Apache-2.0 + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Files: vendor/arrow-array-58.4.0/* +Copyright: 2020-2022 Oliver Margetts +License: Apache-2.0 and Expat + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + MIT License + . + Copyright (c) 2020-2022 Oliver Margetts + . + 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. + Files: vendor/ashpd-0.13.13/* Copyright: 2020 Bilal Elmoussaoui License: Expat @@ -853,6 +979,12 @@ Files: vendor/async-trait-0.1.92/* vendor/event-listener-strategy-0.5.4/* vendor/fastrand-2.5.0/* vendor/itoa-1.0.18/* + vendor/lexical-core-1.0.6/* + vendor/lexical-parse-float-1.0.6/* + vendor/lexical-parse-integer-1.0.6/* + vendor/lexical-util-1.0.7/* + vendor/lexical-write-float-1.0.6/* + vendor/lexical-write-integer-1.0.6/* vendor/once_cell-1.21.4/* vendor/ordered-stream-0.2.0/* vendor/parking-2.2.1/* @@ -878,11 +1010,13 @@ Files: vendor/async-trait-0.1.92/* vendor/typed-path-0.12.3/* Copyright: the authors of async-trait, clickhouse, clickhouse-macros, clickhouse-types, displaydoc, event-listener, event-listener-strategy, - fastrand, itoa, once_cell, ordered-stream, parking, pin-project-lite, - proc-macro-crate, proc-macro2, quote, relm4, relm4-macros, rustversion, - serde, serde_bytes, serde_core, serde_derive, serde_derive_internals, - serde_json, serde_repr, sqlformat, syn, thiserror, thiserror-impl, - typed-path + fastrand, itoa, lexical-core, lexical-parse-float, + lexical-parse-integer, lexical-util, lexical-write-float, + lexical-write-integer, once_cell, ordered-stream, parking, + pin-project-lite, proc-macro-crate, proc-macro2, quote, relm4, + relm4-macros, rustversion, serde, serde_bytes, serde_core, serde_derive, + serde_derive_internals, serde_json, serde_repr, sqlformat, syn, + thiserror, thiserror-impl, typed-path License: Apache-2.0 or Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -1726,6 +1860,35 @@ License: Expat OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/cast-0.3.0/* +Copyright: 2014-2017 Jorge Aparicio +License: Apache-2.0 or Expat + Copyright (c) 2014-2017 Jorge Aparicio + . + 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. + Files: vendor/cbc-0.1.2/* Copyright: 2018-2022 RustCrypto Developers 2018 Artyom Pavlov @@ -1903,6 +2066,31 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/comfy-table-7.1.4/* +Copyright: 2019 Arne Beer +License: Expat + MIT License + . + Copyright (c) 2019 Arne Beer + . + 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. + Files: vendor/const-oid-0.10.2/* vendor/cpufeatures-0.3.1/* vendor/der-0.8.2/* @@ -2159,6 +2347,31 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/crossterm-0.28.1/* +Copyright: 2019 Timon +License: Expat + MIT License + . + Copyright (c) 2019 Timon + . + 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. + Files: vendor/crypto-bigint-0.7.5/* vendor/spki-0.8.0/* Copyright: 2021-2026 The RustCrypto Project Developers @@ -2429,6 +2642,17 @@ License: Expat OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/duckdb-1.10506.1/* +Copyright: 2021-2026 Stichting DuckDB Foundation +License: Expat + Copyright 2021-2026 Stichting DuckDB Foundation + . + 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. + Files: vendor/either-1.18.0/* Copyright: 2015 License: Apache-2.0 or Expat @@ -2716,6 +2940,7 @@ License: Apache-2.0 or Expat DEALINGS IN THE SOFTWARE. Files: vendor/fallible-iterator-0.2.0/* + vendor/fallible-iterator-0.3.0/* Copyright: 2015 The rust-openssl-verify Developers License: Apache-2.0 or Expat Copyright (c) 2015 The rust-openssl-verify Developers @@ -2738,6 +2963,29 @@ License: Apache-2.0 or Expat OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/fallible-streaming-iterator-0.1.9/* +Copyright: 2016 The fallible-streaming-iterator Developers +License: Apache-2.0 or Expat + Copyright (c) 2016 The fallible-streaming-iterator Developers + . + 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. + Files: vendor/field-offset-0.3.6/* Copyright: 2016-2021 Diggory Blake, and other contributors. License: Apache-2.0 or Expat @@ -2835,38 +3083,6 @@ License: Unicode-3.0 Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. -Files: vendor/flagset-0.4.7/* - vendor/fragile-2.1.0/* -Copyright: the authors of flagset, fragile -License: Apache-2.0 - On Debian systems, the complete text of the Apache License, Version 2.0 - can be found in "/usr/share/common-licenses/Apache-2.0". - . - APPENDIX: How to apply the Apache License to your work. - . - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - . - Copyright [yyyy] [name of copyright owner] - . - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - . - http://www.apache.org/licenses/LICENSE-2.0 - . - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - Files: vendor/flate2-1.1.10/* Copyright: 2014-2026 Alex Crichton License: Apache-2.0 or Expat @@ -2896,7 +3112,8 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: vendor/foldhash-0.2.0/* +Files: vendor/foldhash-0.1.5/* + vendor/foldhash-0.2.0/* Copyright: 2024 Orson Peters License: Zlib Copyright (c) 2024 Orson Peters @@ -3152,7 +3369,8 @@ License: Apache-2.0 or Expat OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: vendor/hashbrown-0.16.1/* +Files: vendor/hashbrown-0.15.5/* + vendor/hashbrown-0.16.1/* vendor/hashbrown-0.17.1/* Copyright: 2016 Amanieu d'Antras License: Apache-2.0 or Expat @@ -3182,7 +3400,8 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: vendor/hashlink-0.11.1/* +Files: vendor/hashlink-0.10.0/* + vendor/hashlink-0.11.1/* Copyright: the authors of hashlink License: Apache-2.0 or Expat This work is derived in part from the `linked-hash-map` crate, Copyright (c) @@ -3216,6 +3435,8 @@ Files: vendor/heck-0.5.0/* vendor/unicode-bidi-0.3.18/* vendor/unicode-normalization-0.1.25/* vendor/unicode-properties-0.1.4/* + vendor/unicode-segmentation-1.13.3/* + vendor/unicode-width-0.2.2/* Copyright: 2015 The Rust Project Developers License: Apache-2.0 or Expat Copyright (c) 2015 The Rust Project Developers @@ -3666,13 +3887,50 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: vendor/libgssapi-0.11.0/* - vendor/libgssapi-sys-0.3.4/* -Copyright: the authors of libgssapi, libgssapi-sys -License: Expat - MIT License +Files: vendor/libduckdb-sys-1.10506.1/* +Copyright: 2021-2026 Stichting DuckDB Foundation + 2018-2025 Stichting DuckDB Foundation + 2009, 2010, 2013-2016 by the Brotli Authors. + 2013-2016, Cameron Desrochers. + 2012 - present, Victor Zverovich + 2018-2020, CWI, TU Munich, FSU Jena + 2017 yhirose + 2012 Art.sy, Inc. + 2022 Max Bachmann + 2015, Lukas Fittl + 2011-2020, Yann Collet + 1989, 1991 Free Software Foundation, Inc., + 2013-2014 RAD Game Tools and Valve Software + 2010-2014 Rich Geldreich and Tenacious Software LLC + 2014 The Apache Software Foundation. + 2013 Daniel Lemire + 2012-2014 Twitter + 2014-2017 Melissa O'Neill and PCG Project contributors + 2021 Orson Peters + Malte Skarupke 2016. + 2017-2023 Paul Ross + 2011, Google Inc. + 2015 Morwenn + 2020 YaoYuan + 2016-present, Facebook, Inc. All rights reserved. +License: Apache-2.0, and Apache-2.0 or GPL-2.0+, and BSD-2-Clause, and BSD-2-Clause or BSL-1.0, and BSD-3-Clause, and BSL-1.0, and Expat, and Unicode-DFS-2015, and Zlib + Copyright 2021-2026 Stichting DuckDB Foundation . - Copyright (c) [year] [fullname] + 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. + . + Copyright 2018-2025 Stichting DuckDB Foundation + . + 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. + . + Copyright (c) 2009, 2010, 2013-2016 by the Brotli 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 @@ -3681,8 +3939,1174 @@ License: Expat 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 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. + . + This license file applies to everything in this repository except that which + is explicitly annotated as being written by other authors, i.e. the Boost + queue (included in the benchmarks for comparison), Intel's TBB library (ditto), + the CDSChecker tool (used for verification), the Relacy model checker (ditto), + and Jeff Preshing's semaphore implementation (used in the blocking queue) which + has a zlib license (embedded in lightweightsempahore.h). + . + --- + . + Simplified BSD License: + . + Copyright (c) 2013-2016, Cameron Desrochers. + All rights reserved. + . + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + . + - Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + . + --- + . + I have also chosen to dual-license under the Boost Software License as an alternative to + the Simplified BSD license above: + . + Boost Software License - Version 1.0 - August 17th, 2003 + . + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + . + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + . + 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + . + 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. + . + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work + . + To apply the Apache License to your work, attach the following boilerplate + notice, with the fields enclosed by brackets "[]" replaced with your own + identifying information. (Don't include the brackets!) The text should be + enclosed in the appropriate comment syntax for the file format. We also + recommend that a file or class name and description of purpose be included on + the same "printed page" as the copyright notice for easier identification within + third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + Copyright (c) 2012 - present, Victor Zverovich + . + 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. + . + --- Optional exception to the license --- + . + As an exception, if, as a result of your compiling your source code, portions + of this Software are embedded into a machine-executable object form of such + source code, you may redistribute such embedded portions in such object form + without including the above copyright and permission notices. + . + MIT License + . + Copyright (c) 2018-2020, CWI, TU Munich, FSU Jena + . + 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. + . + The MIT License (MIT) + . + Copyright (c) 2017 yhirose + . + 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. + . + Copyright (c) 2012 Art.sy, Inc. + . + 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. + . + Copyright © 2022 Max Bachmann + . + 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. + . + Copyright (c) 2015, Lukas Fittl + All rights reserved. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + * Neither the name of pg_query nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + . + LZ4 Library + Copyright (c) 2011-2020, Yann Collet + All rights reserved. + . + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + * Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + . + Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) + OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license. + This means that users may choose which of these licenses they take the code + under. + . + The full text of each of these licenses is given below. + . + . + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + . + =============================================================================== + . + . + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + . + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + . + Preamble + . + The licenses for most software are designed to take away your + freedom to share and change it. By contrast, the GNU General Public + License is intended to guarantee your freedom to share and change free + software--to make sure the software is free for all its users. This + General Public License applies to most of the Free Software + Foundation's software and to any other program whose authors commit to + using it. (Some other Free Software Foundation software is covered by + the GNU Lesser General Public License instead.) You can apply it to + your programs, too. + . + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + this service if you wish), that you receive source code or can get it + if you want it, that you can change the software or use pieces of it + in new free programs; and that you know you can do these things. + . + To protect your rights, we need to make restrictions that forbid + anyone to deny you these rights or to ask you to surrender the rights. + These restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + . + For example, if you distribute copies of such a program, whether + gratis or for a fee, you must give the recipients all the rights that + you have. You must make sure that they, too, receive or can get the + source code. And you must show them these terms so they know their + rights. + . + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + . + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + . + Finally, any free program is threatened constantly by software + patents. We wish to avoid the danger that redistributors of a free + program will individually obtain patent licenses, in effect making the + program proprietary. To prevent this, we have made it clear that any + patent must be licensed for everyone's free use or not licensed at all. + . + The precise terms and conditions for copying, distribution and + modification follow. + . + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + . + 0. This License applies to any program or other work which contains + a notice placed by the copyright holder saying it may be distributed + under the terms of this General Public License. The "Program", below, + refers to any such program or work, and a "work based on the Program" + means either the Program or any derivative work under copyright law: + that is to say, a work containing the Program or a portion of it, + either verbatim or with modifications and/or translated into another + language. (Hereinafter, translation is included without limitation in + the term "modification".) Each licensee is addressed as "you". + . + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of + running the Program is not restricted, and the output from the Program + is covered only if its contents constitute a work based on the + Program (independent of having been made by running the Program). + Whether that is true depends on what the Program does. + . + 1. You may copy and distribute verbatim copies of the Program's + source code as you receive it, in any medium, provided that you + conspicuously and appropriately publish on each copy an appropriate + copyright notice and disclaimer of warranty; keep intact all the + notices that refer to this License and to the absence of any warranty; + and give any other recipients of the Program a copy of this License + along with the Program. + . + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + . + 2. You may modify your copy or copies of the Program or any portion + of it, thus forming a work based on the Program, and copy and + distribute such modifications or work under the terms of Section 1 + above, provided that you also meet all of these conditions: + . + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + . + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + . + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + . + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, + and can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based + on the Program, the distribution of the whole must be on the terms of + this License, whose permissions for other licensees extend to the + entire whole, and thus to each and every part regardless of who wrote it. + . + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + . + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of + a storage or distribution medium does not bring the other work under + the scope of this License. + . + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + . + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + . + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + . + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + . + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source + code means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to + control compilation and installation of the executable. However, as a + special exception, the source code distributed need not include + anything that is normally distributed (in either source or binary + form) with the major components (compiler, kernel, and so on) of the + operating system on which the executable runs, unless that component + itself accompanies the executable. + . + If distribution of executable or object code is made by offering + access to copy from a designated place, then offering equivalent + access to copy the source code from the same place counts as + distribution of the source code, even though third parties are not + compelled to copy the source along with the object code. + . + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense or distribute the Program is + void, and will automatically terminate your rights under this License. + However, parties who have received copies, or rights, from you under + this License will not have their licenses terminated so long as such + parties remain in full compliance. + . + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and + all its terms and conditions for copying, distributing or modifying + the Program or works based on it. + . + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further + restrictions on the recipients' exercise of the rights granted herein. + You are not responsible for enforcing compliance by third parties to + this License. + . + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot + distribute so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you + may not distribute the Program at all. For example, if a patent + license would not permit royalty-free redistribution of the Program by + all those who receive copies directly or indirectly through you, then + the only way you could satisfy both it and this License would be to + refrain entirely from distribution of the Program. + . + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + . + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is + implemented by public license practices. Many people have made + generous contributions to the wide range of software distributed + through that system in reliance on consistent application of that + system; it is up to the author/donor to decide if he or she is willing + to distribute software through any other system and a licensee cannot + impose that choice. + . + This section is intended to make thoroughly clear what is believed to + be a consequence of the rest of this License. + . + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License + may add an explicit geographical distribution limitation excluding + those countries, so that distribution is permitted only in or among + countries not thus excluded. In such case, this License incorporates + the limitation as if written in the body of this License. + . + 9. The Free Software Foundation may publish revised and/or new versions + of the General Public License from time to time. Such new versions will + be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + . + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and conditions + either of that version or of any later version published by the Free + Software Foundation. If the Program does not specify a version number of + this License, you may choose any version ever published by the Free Software + Foundation. + . + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the author + to ask for permission. For software which is copyrighted by the Free + Software Foundation, write to the Free Software Foundation; we sometimes + make exceptions for this. Our decision will be guided by the two goals + of preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + . + NO WARRANTY + . + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED + OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS + TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE + PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. + . + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, + INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING + OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY + YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + . + END OF TERMS AND CONDITIONS + . + How to Apply These Terms to Your New Programs + . + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + . + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + convey the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + . + + Copyright (C) + . + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + . + Also add information on how to contact you by electronic and paper mail. + . + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + . + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + . + The hypothetical commands `show w' and `show c' should show the appropriate + parts of the General Public License. Of course, the commands you use may + be called something other than `show w' and `show c'; they could even be + mouse-clicks or menu items--whatever suits your program. + . + You should also get your employer (if you work as a programmer) or your + school, if any, to sign a "copyright disclaimer" for the program, if + necessary. Here is a sample; alter the names: + . + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + . + , 1 April 1989 + Ty Coon, President of Vice + . + This General Public License does not permit incorporating your program into + proprietary programs. If your program is a subroutine library, you may + consider it more useful to permit linking proprietary applications with the + library. If this is what you want to do, use the GNU Lesser General + Public License instead of this License. + . + Copyright 2013-2014 RAD Game Tools and Valve Software + Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + . + 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. + . + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + -------------------------------------------------------------------------------- + . + This product includes code from Apache Avro. + . + Copyright: 2014 The Apache Software Foundation. + Home page: https://avro.apache.org/ + License: http://www.apache.org/licenses/LICENSE-2.0 + . + -------------------------------------------------------------------------------- + . + This project includes code from Daniel Lemire's JavaFastPFOR project. The + "Lemire" bit packing source code produced by parquet-generator is derived from + the JavaFastPFOR project. + . + Copyright: 2013 Daniel Lemire + Home page: http://lemire.me/en/ + Project page: https://github.com/lemire/JavaFastPFOR + License: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + . + -------------------------------------------------------------------------------- + . + This product includes code from Apache Spark. + . + * dev/merge_parquet_pr.py is based on Spark's dev/merge_spark_pr.py + . + Copyright: 2014 The Apache Software Foundation. + Home page: https://spark.apache.org/ + License: http://www.apache.org/licenses/LICENSE-2.0 + . + -------------------------------------------------------------------------------- + . + This product includes code from Twitter's ElephantBird project. + . + * parquet-hadoop's UnmaterializableRecordCounter.java includes code from + ElephantBird's LzoRecordReader.java + . + Copyright: 2012-2014 Twitter + Home page: https://github.com/twitter/elephant-bird + License: http://www.apache.org/licenses/LICENSE-2.0 + . + Copyright (c) 2014-2017 Melissa O'Neill and PCG Project 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. + . + Copyright (c) 2021 Orson Peters + . + This software is provided 'as-is', without any express or implied warranty. In no event will the + authors be held liable for any damages arising from the use of this software. + . + Permission is granted to anyone to use this software for any purpose, including commercial + applications, and to alter it and redistribute it freely, subject to the following restrictions: + . + 1. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + . + 2. Altered source versions must be plainly marked as such, and must not be misrepresented as + being the original software. + . + 3. This notice may not be removed or altered from any source distribution. + . + // Copyright (c) 2009 The RE2 Authors. All rights reserved. + // + // Redistribution and use in source and binary forms, with or without + // modification, are permitted provided that the following conditions are + // met: + // + // * Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimer. + // * Redistributions in binary form must reproduce the above + // copyright notice, this list of conditions and the following disclaimer + // in the documentation and/or other materials provided with the + // distribution. + // * Neither the name of Google Inc. nor the names of its + // contributors may be used to endorse or promote products derived from + // this software without specific prior written permission. + // + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + . + Copyright Malte Skarupke 2016. + Distributed under the Boost Software License, Version 1.0. + (See http://www.boost.org/LICENSE_1_0.txt) + . + MIT License + . + Copyright (c) 2017-2023 Paul Ross + . + 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. + . + Copyright 2011, Google Inc. + All rights reserved. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + . + === + . + Some of the benchmark data in testdata/ is licensed differently: + . + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + . + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + . + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + . + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). + . + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + The Java version of the t-digest was originally authored by Ted Dunning + . + A number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) to the Java version. + . + The C++ version herein is a derivative of the Java version. It was written by Derrick R. Burns (https:://github.com/derrickburns). + The main modifications are 1) higher performance multi- t-digest merging and 2) faster quantile() and cdf() computation. + . + On Debian systems, the complete text of the Apache License, Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + -------------------------------------------------- + SOFTWARE DISTRIBUTED WITH THRIFT: + . + The Apache Thrift software includes a number of subcomponents with + separate copyright notices and license terms. Your use of the source + code for the these subcomponents is subject to the terms and + conditions of the following licenses. + . + -------------------------------------------------- + Portions of the following files are licensed under the MIT License: + . + lib/erl/src/Makefile.am + . + Please see doc/otp-base-license.txt for the full terms of this license. + . + -------------------------------------------------- + For the aclocal/ax_boost_base.m4 and contrib/fb303/aclocal/ax_boost_base.m4 components: + . + # Copyright (c) 2007 Thomas Porschberg + # + # Copying and distribution of this file, with or without + # modification, are permitted in any medium without royalty provided + # the copyright notice and this notice are preserved. + . + -------------------------------------------------- + For the lib/nodejs/lib/thrift/json_parse.js: + . + /* + json_parse.js + 2015-05-02 + Public Domain. + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + . + */ + (By Douglas Crockford ) + -------------------------------------------------- + . + ## utf8proc license ## + . + **utf8proc** is a software package originally developed + by Jan Behrens and the rest of the Public Software Group, who + deserve nearly all of the credit for this library, that is now maintained by the Julia-language developers. Like the original utf8proc, + whose copyright and license statements are reproduced below, all new + work on the utf8proc library is licensed under the [MIT "expat" + license](http://opensource.org/licenses/MIT): + . + *Copyright © 2014-2019 by Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas Fonseca, and other contributors listed in the git history.* + . + 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. + . + ## Original utf8proc license ## + . + *Copyright (c) 2009, 2013 Public Software Group e. V., Berlin, Germany* + . + 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. + . + ## Unicode data license ## + . + This software contains data (`utf8proc_data.c`) derived from processing + the Unicode data files. The following license applies to that data: + . + **COPYRIGHT AND PERMISSION NOTICE** + . + *Copyright (c) 1991-2007 Unicode, Inc. All rights reserved. Distributed + under the Terms of Use in http://www.unicode.org/copyright.html.* + . + Permission is hereby granted, free of charge, to any person obtaining a + copy of the Unicode data files and any associated documentation (the "Data + Files") or Unicode software and any associated documentation (the + "Software") to deal in the Data Files or Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, and/or sell copies of the Data Files or Software, and + to permit persons to whom the Data Files or Software are furnished to do + so, provided that (a) the above copyright notice(s) and this permission + notice appear with all copies of the Data Files or Software, (b) both the + above copyright notice(s) and this permission notice appear in associated + documentation, and (c) there is clear notice in each modified Data File or + in the Software as well as in the documentation associated with the Data + File(s) or Software that the data or software has been modified. + . + THE DATA FILES AND SOFTWARE ARE 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 OF + THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL 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 THE DATA FILES OR SOFTWARE. + . + Except as contained in this notice, the name of a copyright holder shall + not be used in advertising or otherwise to promote the sale, use or other + dealings in these Data Files or Software without prior written + authorization of the copyright holder. + . + Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be + registered in some jurisdictions. All other trademarks and registered + trademarks mentioned herein are the property of their respective owners. + . + The MIT License (MIT) + . + Copyright (c) 2015 Morwenn + . + 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. + . + MIT License + . + Copyright (c) 2020 YaoYuan + . + 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. + . + BSD License + . + For Zstandard software + . + Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + . + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Files: vendor/libgssapi-0.11.0/* + vendor/libgssapi-sys-0.3.4/* +Copyright: the authors of libgssapi, libgssapi-sys +License: Expat + MIT License + . + Copyright (c) [year] [fullname] + . + 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, @@ -3811,6 +5235,8 @@ License: Expat THE SOFTWARE. Files: vendor/linux-raw-sys-0.12.1/* + vendor/linux-raw-sys-0.4.15/* + vendor/rustix-0.38.44/* vendor/rustix-1.1.4/* Copyright: the authors of linux-raw-sys, rustix License: Apache-2.0 or Apache-2.0 with LLVM exception or Expat @@ -4879,6 +6305,32 @@ License: Apache-2.0 or Expat OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Files: vendor/strum-0.27.2/* + vendor/strum_macros-0.27.2/* +Copyright: 2019 Peter Glotfelty +License: Expat + MIT License + . + Copyright (c) 2019 Peter Glotfelty + . + 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. + Files: vendor/subtle-2.6.1/* Copyright: 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. 2016-2024 Isis Agora Lovecruft. All rights reserved. diff --git a/linux/build-aux/packaging/bundled_licenses.py b/linux/build-aux/packaging/bundled_licenses.py new file mode 100644 index 0000000000..0ed7f4b339 --- /dev/null +++ b/linux/build-aux/packaging/bundled_licenses.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# Adds the licenses of C and C++ code a crate compiles from its own copy +# of another project to data/licenses. cargo-about reads only the crate's +# license file, and libduckdb-sys packs DuckDB's third_party code, whose +# licenses are not in the crate, into one archive. Each folder under +# build-aux/about/bundled names a crate version and holds those texts as +# the bundled project ships them. + +import argparse +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import license_metadata # noqa: E402 + +SECTION = "## Code compiled into crates" +SECTION_INTRO = ( + "These crates compile C and C++ code from other projects into TablePro. " + "Each project is listed under the license it is used under, with the " + "file its text comes from." +) + + +class BundledError(Exception): + pass + + +def load_bundles(root): + bundles = [] + for manifest_path in sorted(Path(root).glob("*/manifest.json")): + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + components = [] + for component in manifest["components"]: + text = (manifest_path.parent / component["file"]).read_text(encoding="utf-8") + components.append({**component, "text": text}) + bundles.append({**manifest, "components": components}) + return bundles + + +def combined_expression(expressions): + nodes = [license_metadata.parse_expression(expression) for expression in expressions] + return license_metadata.spdx(license_metadata.combine("and", nodes)) + + +def merge_json(data, bundles): + # Records added by an earlier run are dropped first, so a second run + # writes the same file. + data["licenses"] = [record for record in data["licenses"] if "component" not in record] + crates = {crate["name"]: crate for crate in data["crates"]} + for bundle in bundles: + crate = crates.get(bundle["crate"]) + if crate is None: + raise BundledError(f"{bundle['crate']} is no longer linked; remove build-aux/about/bundled/{bundle['crate']}") + if crate["version"] != bundle["version"]: + raise BundledError( + f"build-aux/about/bundled/{bundle['crate']} is for {bundle['version']}, but {crate['version']} is " + "linked; take the license files of the code the new version bundles" + ) + for component in bundle["components"]: + data["licenses"].append( + { + "id": component["id"], + "name": component["license_name"], + "text": component["text"], + "used_by": [{"crate": {"name": crate["name"], "version": crate["version"]}}], + "component": component["name"], + } + ) + crate["license"] = combined_expression( + [crate["license"]] + [component["license"] for component in bundle["components"]] + ) + return data + + +def render_section(bundles): + lines = [SECTION, "", SECTION_INTRO] + for bundle in bundles: + for component in bundle["components"]: + lines += [ + "", + f"### {component['name']}: {component['license_name']}", + "", + f"Compiled into {bundle['crate']} {bundle['version']}, from `{component['path']}` in {bundle['source']}.", + "", + "~~~~text", + component["text"].rstrip("\n"), + "~~~~", + ] + return "\n".join(lines) + "\n" + + +def scalar(value): + return json.dumps(value, ensure_ascii=False) + + +def render_json(data): + # The layout of build-aux/about/third-party.json.hbs, so a crate change + # shows as the lines it changes. + records = [] + for record in data["licenses"]: + used_by = ",\n".join( + f' {{ "crate": {{ "name": {scalar(used["crate"]["name"])}, ' + f'"version": {scalar(used["crate"]["version"])} }} }}' + for used in record["used_by"] + ) + fields = [f' "{key}": {scalar(record[key])}' for key in ("id", "name", "text")] + if "component" in record: + fields.append(f' "component": {scalar(record["component"])}') + fields.append(f' "used_by": [\n{used_by}\n ]') + records.append(" {\n" + ",\n".join(fields) + "\n }") + crates = ",\n".join( + f' {{ "name": {scalar(crate["name"])}, "version": {scalar(crate["version"])}, ' + f'"license": {scalar(crate["license"])} }}' + for crate in data["crates"] + ) + return ( + '{\n "licenses": [\n' + ",\n".join(records) + '\n ],\n "crates": [\n' + crates + "\n ]\n}\n" + ) + + +def merge_notice(notice, bundles): + kept = notice.split(f"\n{SECTION}\n", 1)[0].rstrip("\n") + return f"{kept}\n\n{render_section(bundles)}" + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Add the licenses of code crates bundle to data/licenses.") + parser.add_argument("--bundled", required=True) + parser.add_argument("--licenses", required=True) + parser.add_argument("--notice", required=True) + args = parser.parse_args(argv) + + try: + bundles = load_bundles(args.bundled) + licenses = Path(args.licenses) + notice = Path(args.notice) + data = merge_json(json.loads(licenses.read_text(encoding="utf-8")), bundles) + merged_notice = merge_notice(notice.read_text(encoding="utf-8"), bundles) + except (OSError, KeyError, ValueError, license_metadata.MetadataError, BundledError) as error: + print(f"bundled_licenses.py: {error}", file=sys.stderr) + return 1 + + licenses.write_text(render_json(data), encoding="utf-8") + notice.write_text(merged_notice, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux/build-aux/packaging/license_metadata.py b/linux/build-aux/packaging/license_metadata.py index 554a9259ec..29ee1f8693 100644 --- a/linux/build-aux/packaging/license_metadata.py +++ b/linux/build-aux/packaging/license_metadata.py @@ -30,7 +30,7 @@ COMMON_LICENSES = {"CC0-1.0": "CC0 1.0 Universal license"} NOTICE = re.compile(r"^(?:copyright\b|©|\(c\)\s*\d)", re.IGNORECASE) -NOTICE_PREFIX = re.compile(r"^(?:copyright\b\s*)?(?:\(c\)|©)?\s*", re.IGNORECASE) +NOTICE_PREFIX = re.compile(r"^(?:copyright\b\s*:?\s*)?(?:\(c\)|©)?\s*", re.IGNORECASE) NOT_A_NOTICE = re.compile( r"\[(?:yyyy|year)\]|\{yyyy\}|" r"|^copyright\s+(?:notices?|holders?|owners?|license|and\s+permission|statement)\b" diff --git a/linux/build-aux/rpm/tablepro.spec b/linux/build-aux/rpm/tablepro.spec index 15b0bb3cad..083e626259 100644 --- a/linux/build-aux/rpm/tablepro.spec +++ b/linux/build-aux/rpm/tablepro.spec @@ -23,17 +23,22 @@ License: %{shrink: (Apache-2.0 OR BSD-2-Clause OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR BSL-1.0 OR MIT) AND + (Apache-2.0 OR GPL-2.0-or-later) AND (Apache-2.0 OR ISC) AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND + BSD-2-Clause AND + (BSD-2-Clause OR BSL-1.0) AND BSD-3-Clause AND + BSL-1.0 AND CDLA-Permissive-2.0 AND ISC AND MIT AND (MIT OR Unlicense) AND Unicode-3.0 AND + Unicode-DFS-2015 AND Zlib } URL: https://tablepro.app @@ -46,6 +51,8 @@ BuildRequires: rust >= 1.97 BuildRequires: cargo-rpm-macros >= 26 BuildRequires: meson >= 1.4 BuildRequires: gcc +# DuckDB is C++, compiled from the duckdb crate's own sources. +BuildRequires: gcc-c++ BuildRequires: clang-devel BuildRequires: gettext BuildRequires: desktop-file-utils @@ -92,9 +99,9 @@ Recommends: (gnome-keyring or kf6-kwallet or keepassxc) Suggests: krb5-workstation %description -TablePro connects to PostgreSQL, MySQL, SQLite, SQL Server and ClickHouse. -Browse tables, edit rows in place, run SQL with syntax highlighting, and -reach servers behind an SSH bastion. +TablePro connects to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server and +ClickHouse. Browse tables, edit rows in place, run SQL with syntax +highlighting, and reach servers behind an SSH bastion. %prep %autosetup -n %{name}-%{version_no_tilde} -a1 diff --git a/linux/build-aux/tests/test_bundled_licenses.py b/linux/build-aux/tests/test_bundled_licenses.py new file mode 100644 index 0000000000..3f670c2004 --- /dev/null +++ b/linux/build-aux/tests/test_bundled_licenses.py @@ -0,0 +1,182 @@ +import contextlib +import io +import json +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "packaging")) + +import bundled_licenses # noqa: E402 +import license_metadata # noqa: E402 + +CRATE_MIT = "Copyright 2021 The Crate Authors\n\nPermission is hereby granted, free of charge.\n" +RE2 = "Copyright (c) 2009 The RE2 Authors. All rights reserved.\n\nRedistribution and use in source and binary forms.\n" +SKA = "Copyright Malte Skarupke 2016.\nDistributed under the Boost Software License, Version 1.0.\n" + +NOTICE = """# Third-party licenses + +## License texts + +### MIT License + +Used by: + +- engine-sys 1.2.3 + +~~~~text +Copyright 2021 The Crate Authors +~~~~ +""" + + +def third_party(version="1.2.3"): + return { + "licenses": [ + { + "id": "MIT", + "name": "MIT License", + "text": CRATE_MIT, + "used_by": [{"crate": {"name": "engine-sys", "version": version}}], + } + ], + "crates": [{"name": "engine-sys", "version": version, "license": "MIT"}], + } + + +def bundles(version="1.2.3"): + component = {"license_name": "BSD 3-Clause \"New\" or \"Revised\" License", "text": RE2} + return [ + { + "crate": "engine-sys", + "version": version, + "source": "https://example.com/engine/tree/abc", + "components": [ + {**component, "name": "RE2", "path": "third_party/re2/LICENSE", "id": "BSD-3-Clause", "license": "BSD-3-Clause"}, + { + "name": "ska_sort", + "path": "third_party/ska_sort/LICENSE", + "id": "BSL-1.0", + "license": "BSL-1.0 AND MIT", + "license_name": "Boost Software License 1.0", + "text": SKA, + }, + ], + } + ] + + +class MergeJsonTest(unittest.TestCase): + def test_bundled_code_is_listed_under_its_crate(self): + data = bundled_licenses.merge_json(third_party(), bundles()) + + added = [record for record in data["licenses"] if "component" in record] + self.assertEqual([record["component"] for record in added], ["RE2", "ska_sort"]) + self.assertEqual(added[0]["text"], RE2) + self.assertEqual(added[0]["used_by"], [{"crate": {"name": "engine-sys", "version": "1.2.3"}}]) + self.assertEqual(data["crates"][0]["license"], "BSD-3-Clause AND BSL-1.0 AND MIT") + + def test_a_second_run_writes_the_same_file(self): + once = bundled_licenses.merge_json(third_party(), bundles()) + twice = bundled_licenses.merge_json(json.loads(json.dumps(once)), bundles()) + + self.assertEqual(bundled_licenses.render_json(twice), bundled_licenses.render_json(once)) + + def test_another_crate_version_needs_its_own_texts(self): + with self.assertRaisesRegex(bundled_licenses.BundledError, "is for 1.2.3, but 2.0.0 is linked"): + bundled_licenses.merge_json(third_party("2.0.0"), bundles()) + + def test_a_crate_no_longer_linked_is_named(self): + data = third_party() + data["crates"] = [] + + with self.assertRaisesRegex(bundled_licenses.BundledError, "engine-sys is no longer linked"): + bundled_licenses.merge_json(data, bundles()) + + def test_the_copyright_file_names_every_bundled_holder(self): + data = bundled_licenses.merge_json(third_party(), bundles()) + + copyright = license_metadata.render_copyright(data["licenses"], data["crates"], "AGPL text\n", "AGPL-3.0-or-later") + + self.assertIn("2009 The RE2 Authors. All rights reserved.", copyright) + self.assertIn("Malte Skarupke 2016.", copyright) + self.assertIn("License: BSD-3-Clause and BSL-1.0 and MIT", copyright) + + +class RenderJsonTest(unittest.TestCase): + def test_the_layout_is_the_templates(self): + template = Path(__file__).resolve().parent.parent.parent / "data" / "licenses" / "third-party.json" + text = template.read_text(encoding="utf-8") + + self.assertEqual(bundled_licenses.render_json(json.loads(text)), text) + + +class MergeNoticeTest(unittest.TestCase): + def test_the_section_follows_the_crate_licenses_and_is_replaced_on_a_second_run(self): + once = bundled_licenses.merge_notice(NOTICE, bundles()) + twice = bundled_licenses.merge_notice(once, bundles()) + + self.assertEqual(once, twice) + self.assertTrue(once.startswith(NOTICE.rstrip("\n") + "\n\n## Code compiled into crates\n")) + self.assertIn("### RE2: BSD 3-Clause", once) + self.assertIn("from `third_party/re2/LICENSE` in https://example.com/engine/tree/abc.", once) + self.assertIn("~~~~text\n" + RE2.rstrip("\n") + "\n~~~~", once) + + +class MainTest(unittest.TestCase): + def test_writes_both_files_from_a_bundle_folder(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + folder = root / "bundled" / "engine-sys" + folder.mkdir(parents=True) + (folder / "re2.txt").write_text(RE2, encoding="utf-8") + component = bundles()[0]["components"][0] + manifest = { + "crate": "engine-sys", + "version": "1.2.3", + "source": "https://example.com/engine/tree/abc", + "components": [{key: value for key, value in component.items() if key != "text"} | {"file": "re2.txt"}], + } + (folder / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + licenses = root / "third-party.json" + licenses.write_text(json.dumps(third_party()), encoding="utf-8") + notice = root / "NOTICE.md" + notice.write_text(NOTICE, encoding="utf-8") + + status = bundled_licenses.main( + ["--bundled", str(root / "bundled"), "--licenses", str(licenses), "--notice", str(notice)] + ) + + self.assertEqual(status, 0) + written = json.loads(licenses.read_text(encoding="utf-8")) + self.assertEqual(written["crates"][0]["license"], "BSD-3-Clause AND MIT") + self.assertIn("### RE2: BSD 3-Clause", notice.read_text(encoding="utf-8")) + + def test_a_stale_bundle_fails_without_writing(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + folder = root / "bundled" / "engine-sys" + folder.mkdir(parents=True) + (folder / "manifest.json").write_text( + json.dumps({"crate": "engine-sys", "version": "0.9.0", "source": "x", "components": []}), + encoding="utf-8", + ) + licenses = root / "third-party.json" + original = json.dumps(third_party()) + licenses.write_text(original, encoding="utf-8") + notice = root / "NOTICE.md" + notice.write_text(NOTICE, encoding="utf-8") + + with contextlib.redirect_stderr(io.StringIO()) as stderr: + status = bundled_licenses.main( + ["--bundled", str(root / "bundled"), "--licenses", str(licenses), "--notice", str(notice)] + ) + + self.assertEqual(status, 1) + self.assertIn("is for 0.9.0, but 1.2.3 is linked", stderr.getvalue()) + self.assertEqual(licenses.read_text(encoding="utf-8"), original) + + +if __name__ == "__main__": + unittest.main() diff --git a/linux/build-aux/tests/test_license_metadata.py b/linux/build-aux/tests/test_license_metadata.py index fef146de32..9a8b00847e 100644 --- a/linux/build-aux/tests/test_license_metadata.py +++ b/linux/build-aux/tests/test_license_metadata.py @@ -280,6 +280,13 @@ def test_copyright_takes_the_notices_without_their_prefix(self): self.assertEqual(paragraph["Copyright"], ["2019 Alice", "1991-2024 Unicode, Inc."]) + def test_a_notice_written_as_a_field_loses_its_colon(self): + text = "This product includes code from Apache Spark.\n\nCopyright: 2014 The Apache Software Foundation.\n" + licenses = [entry("MIT", text, crate("a", "1.0.0"))] + paragraph = files_paragraph(self.render(licenses), "vendor/a-1.0.0/*") + + self.assertEqual(paragraph["Copyright"], ["2014 The Apache Software Foundation."]) + def test_a_text_without_a_notice_credits_the_crates_authors(self): licenses = [entry("MIT", MIT_NO_NOTICE, crate("serde", "1.0.0"), crate("syn", "1.0.0"), crate("syn", "2.0.0"))] paragraph = files_paragraph(self.render(licenses), "vendor/serde-1.0.0/*") diff --git a/linux/build-aux/update-third-party-licenses.sh b/linux/build-aux/update-third-party-licenses.sh index e12fbb491f..27a5ebfc70 100755 --- a/linux/build-aux/update-third-party-licenses.sh +++ b/linux/build-aux/update-third-party-licenses.sh @@ -45,6 +45,13 @@ for output in third-party.json THIRD-PARTY-NOTICE.md; do generate "$root/build-aux/about/$output.hbs" "$root/data/licenses/$output" done +# cargo-about cannot read the licenses of code a crate bundles in an +# archive, such as DuckDB's third_party in libduckdb-sys. +python3 "$root/build-aux/packaging/bundled_licenses.py" \ + --bundled "$root/build-aux/about/bundled" \ + --licenses "$root/data/licenses/third-party.json" \ + --notice "$root/data/licenses/THIRD-PARTY-NOTICE.md" + python3 "$root/build-aux/packaging/license_metadata.py" write \ --licenses "$root/data/licenses/third-party.json" \ --license-file "$root/LICENSE" \ diff --git a/linux/crates/app/Cargo.toml b/linux/crates/app/Cargo.toml index 90252994ac..703bb33b26 100644 --- a/linux/crates/app/Cargo.toml +++ b/linux/crates/app/Cargo.toml @@ -60,6 +60,7 @@ kerberos = ["tablepro-driver-mssql/kerberos"] [dependencies] tablepro-core.workspace = true tablepro-driver-clickhouse.workspace = true +tablepro-driver-duckdb.workspace = true tablepro-driver-mssql.workspace = true tablepro-driver-mysql.workspace = true tablepro-driver-postgres.workspace = true diff --git a/linux/crates/app/src/application/app_services.rs b/linux/crates/app/src/application/app_services.rs index 7d55615207..0d656b926c 100644 --- a/linux/crates/app/src/application/app_services.rs +++ b/linux/crates/app/src/application/app_services.rs @@ -79,11 +79,12 @@ impl AppServices { } = parts; let application_hold = Rc::new(GioProcessHold::default()); let open_sessions = OpenSessions::new(storage.workspaces().clone(), application_hold.clone()); - let saved_connections = SavedConnectionList::follow(storage.connections()); + let drivers = Arc::new(drivers); + let saved_connections = SavedConnectionList::follow(storage.connections(), drivers.clone()); let saved_connection_editor = SavedConnectionEditor::new(storage.connections().clone(), storage.secrets().clone(), tasks.clone()); Self { - drivers: Arc::new(drivers), + drivers, connector, deadlines, liveness, diff --git a/linux/crates/app/src/lib.rs b/linux/crates/app/src/lib.rs index 539c047974..b11423c095 100644 --- a/linux/crates/app/src/lib.rs +++ b/linux/crates/app/src/lib.rs @@ -69,6 +69,7 @@ pub fn run() -> Result { fn build_registry() -> DriverRegistry { let mut r = DriverRegistry::new(); r.register(Arc::new(tablepro_driver_clickhouse::ClickhouseDriver)); + r.register(Arc::new(tablepro_driver_duckdb::DuckDbDriver)); r.register(Arc::new(tablepro_driver_mssql::MssqlDriver)); r.register(Arc::new(tablepro_driver_mysql::MysqlDriver)); r.register(Arc::new(tablepro_driver_postgres::PgDriver)); @@ -92,6 +93,36 @@ mod tests { use super::register_test_resources; use super::*; + /// Which engine opens a file handed over is decided by its header, + /// then its name, so no two file engines may claim the same bytes + /// or the same suffix. + #[test] + fn file_engines_claim_disjoint_signatures_and_suffixes() { + let registry = build_registry(); + let formats: Vec<(&str, tablepro_core::FileFormat)> = registry + .iter() + .filter_map(|driver| driver.capabilities().file_format().map(|format| (driver.id(), *format))) + .collect(); + + assert!(formats.len() >= 2, "{formats:?}"); + for (index, (id, format)) in formats.iter().enumerate() { + for (other_id, other) in &formats[index + 1..] { + let mut header = vec![0u8; format.signature.header_len().max(other.signature.header_len())]; + header[format.signature.offset..format.signature.header_len()].copy_from_slice(format.signature.bytes); + assert!( + !other.signature.matches(&header), + "{id} and {other_id} share a signature" + ); + for suffix in format.suffixes { + assert!( + !other.suffixes.contains(suffix), + "{id} and {other_id} both claim .{suffix}" + ); + } + } + } + } + #[test] fn resources_contain_style_css() { register_test_resources(); diff --git a/linux/crates/app/src/logging.rs b/linux/crates/app/src/logging.rs index 211130760a..614a3382d0 100644 --- a/linux/crates/app/src/logging.rs +++ b/linux/crates/app/src/logging.rs @@ -16,8 +16,10 @@ use crate::config::Profile; /// a warning, tiberius rows at trace and server messages as errors, the /// clickhouse crate exceptions that quote the query, deadpool the errors /// of the connections it recycles, and sqlparser at debug the text it -/// parses and the tokens and literals it reads. The drivers map and log -/// what they need from these libraries themselves. +/// parses and the tokens and literals it reads. The duckdb crate logs +/// nothing today, and is silenced so a release that starts to cannot +/// write statements to the journal. The drivers map and log what they +/// need from these libraries themselves. const SILENCED_LIBRARIES: &[&str] = &[ "tokio_postgres", "deadpool", @@ -25,6 +27,7 @@ const SILENCED_LIBRARIES: &[&str] = &[ "sqlx", "clickhouse", "sqlparser", + "duckdb", ]; #[derive(Debug, ThisError)] diff --git a/linux/crates/app/src/services/file_location.rs b/linux/crates/app/src/services/file_location.rs index 8f8c7a7930..0e9bcfb09d 100644 --- a/linux/crates/app/src/services/file_location.rs +++ b/linux/crates/app/src/services/file_location.rs @@ -15,7 +15,7 @@ pub type LocationClassifier = fn(&Path) -> FileLocationKind; /// GIO's name for the portal's `user.document-portal.host-path`. const HOST_PATH_ATTRIBUTE: &str = "xattr::document-portal.host-path"; -/// Filesystems whose locks SQLite cannot rely on across machines. +/// Filesystems whose locks the engines cannot rely on across machines. const REMOTE_FILESYSTEMS: &[&str] = &[ "nfs", "nfs4", @@ -67,7 +67,7 @@ fn classify_with(path: &Path, mount_kind: impl Fn(&Path) -> FileLocationKind) -> } } -/// SQLite opens the file a link points to, while GIO finds the mount +/// An engine opens the file a link points to, while GIO finds the mount /// of the link itself. A path that does not resolve, such as a file /// not made yet, stays as given, and GIO resolves its folder. fn follow_links(path: &Path) -> PathBuf { diff --git a/linux/crates/app/src/services/file_location_kind.rs b/linux/crates/app/src/services/file_location_kind.rs index e4127f7c0d..ed40a22922 100644 --- a/linux/crates/app/src/services/file_location_kind.rs +++ b/linux/crates/app/src/services/file_location_kind.rs @@ -1,11 +1,11 @@ use std::path::{Path, PathBuf}; -/// The filesystem a database file is on, as far as SQLite's locking -/// can trust it. +/// The filesystem a database file is on, as far as the file locks +/// SQLite and DuckDB rely on can trust it. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FileLocationKind { Local, - /// A network filesystem, whose locks SQLite cannot rely on. + /// A network filesystem, whose locks the engines cannot rely on. Remote { fs_type: String, }, @@ -40,7 +40,7 @@ impl FileLocationKind { } } - /// SQLite locking through the portal's FUSE layer is not + /// Locking through the portal's FUSE layer is not /// established, and the host file may be on a network share. pub fn requires_read_only_prompt(&self) -> bool { match self { diff --git a/linux/crates/app/src/services/file_resolution.rs b/linux/crates/app/src/services/file_resolution.rs index 018140b497..7a5bb501a4 100644 --- a/linux/crates/app/src/services/file_resolution.rs +++ b/linux/crates/app/src/services/file_resolution.rs @@ -1,9 +1,11 @@ //! A database file handed to TablePro, resolved to the session it //! opens. Resolving reads the disk, so it runs on the blocking pool. +use std::io::Read; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; -use tablepro_core::DriverRegistry; +use tablepro_core::{BusyReason, DriverError, DriverRegistry}; use tablepro_session::SessionKey; use tablepro_session::registry::FileIdentity; use tablepro_storage::{ConnectionStore, SavedConnection}; @@ -82,12 +84,88 @@ pub fn resolve_file_blocking( ResolvedFile { uri, target } } +/// The first `len` bytes of the file, or as many as it has, which say +/// which engine opens it. +/// +/// SQLite and DuckDB lock a file with POSIX record locks, which belong to +/// the process: closing any descriptor this process has on the file +/// drops them all. A file this process already holds open, through +/// another name such as a hard link, is refused rather than read. +pub fn read_header_blocking(path: &Path, len: usize) -> Result, DriverError> { + let failed = |error: std::io::Error| { + let path = path.to_path_buf(); + match error.kind() { + std::io::ErrorKind::NotFound => DriverError::FileNotFound { path }, + std::io::ErrorKind::PermissionDenied => DriverError::FileAccessDenied { path }, + std::io::ErrorKind::IsADirectory => DriverError::NotADatabase { path }, + kind => DriverError::FileIo { path, kind }, + } + }; + let metadata = std::fs::metadata(path).map_err(failed)?; + if metadata.is_dir() { + return Err(DriverError::NotADatabase { + path: path.to_path_buf(), + }); + } + if held_by_this_process(&metadata) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + let mut header = Vec::with_capacity(len); + std::fs::File::open(path) + .and_then(|file| file.take(len as u64).read_to_end(&mut header)) + .map_err(failed)?; + Ok(header) +} + +/// Whether a descriptor this process has open names the file. +fn held_by_this_process(file: &std::fs::Metadata) -> bool { + let Ok(descriptors) = std::fs::read_dir("/proc/self/fd") else { + return false; + }; + descriptors.flatten().any(|descriptor| { + std::fs::metadata(descriptor.path()).is_ok_and(|held| held.dev() == file.dev() && held.ino() == file.ino()) + }) +} + #[cfg(test)] mod tests { use tablepro_test_support::{ConnectionScript, FakeDriver}; use super::*; + #[test] + fn a_header_is_at_most_what_the_file_holds() { + let directory = tempfile::tempdir().expect("tempdir"); + let short = directory.path().join("short.db"); + std::fs::write(&short, b"abc").expect("seed"); + let missing = directory.path().join("missing.db"); + + assert_eq!(read_header_blocking(&short, 16).expect("the header"), b"abc"); + assert_eq!( + read_header_blocking(&missing, 16), + Err(DriverError::FileNotFound { path: missing }) + ); + } + + /// Reading a file through a second name would close a descriptor on + /// it, which drops every lock this process holds on it. + #[test] + fn a_file_this_process_holds_is_not_read() { + let directory = tempfile::tempdir().expect("tempdir"); + let held = directory.path().join("held.db"); + std::fs::write(&held, b"SQLite format 3\0").expect("seed"); + let linked = directory.path().join("linked.db"); + std::fs::hard_link(&held, &linked).expect("link"); + let open = std::fs::File::open(&held).expect("open"); + + assert_eq!( + read_header_blocking(&linked, 16), + Err(DriverError::Busy(BusyReason::FileLocked)) + ); + drop(open); + assert_eq!(read_header_blocking(&linked, 16).expect("the header"), b"SQLite format 3\0"); + } + #[test] fn a_missing_file_is_not_found() { let root = tempfile::tempdir().expect("tempdir"); diff --git a/linux/crates/app/src/services/saved_connection_list.rs b/linux/crates/app/src/services/saved_connection_list.rs index eedf1cf877..073bc973de 100644 --- a/linux/crates/app/src/services/saved_connection_list.rs +++ b/linux/crates/app/src/services/saved_connection_list.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use gtk4::prelude::*; use gtk4::{gio, glib}; +use tablepro_core::DriverRegistry; use tablepro_storage::{ConnectionGroupName, ConnectionListState, ConnectionStore, SavedConnection}; use uuid::Uuid; @@ -20,6 +21,8 @@ type StateObserver = Rc; /// delete or an edit reaches every picker without anyone asking. pub struct SavedConnectionList { store: gio::ListStore, + /// Says which connections open a file, which their rows name. + drivers: Arc, /// The last list the store had ready, in display order. A list that /// cannot be read keeps it, as the rows do. connections: RefCell>, @@ -37,11 +40,12 @@ impl SavedConnectionList { /// Follow `connections` from its list now. The watch runs on the GTK /// thread and polls nothing but the store's channel, and it ends /// with the store or with this list. - pub fn follow(connections: &ConnectionStore) -> Rc { + pub fn follow(connections: &ConnectionStore, drivers: Arc) -> Rc { let mut receiver = connections.subscribe(); let current = receiver.borrow_and_update().state.clone(); let list = Rc::new(Self { store: gio::ListStore::new::(), + drivers, connections: RefCell::new(Arc::from([])), state: RefCell::new(ConnectionListState::Loading), watch: RefCell::new(None), @@ -115,11 +119,10 @@ impl SavedConnectionList { let sorted: Arc<[SavedConnection]> = Arc::from(sorted); self.connections.replace(sorted.clone()); if regrouped || self.store.n_items() == 0 { - let objects: Vec = - sorted.iter().cloned().map(SavedConnectionObject::new).collect(); + let objects: Vec = sorted.iter().map(|saved| self.object(saved)).collect(); self.store.splice(0, self.store.n_items(), &objects); } else { - patch(&self.store, &sorted); + patch(&self.store, &sorted, |saved| self.object(saved)); } } let observers: Vec = self @@ -134,9 +137,23 @@ impl SavedConnectionList { } } +impl SavedConnectionList { + fn object(&self, saved: &SavedConnection) -> SavedConnectionObject { + let file_based = self + .drivers + .get(&saved.driver_id) + .is_some_and(|driver| driver.capabilities().is_file_based()); + SavedConnectionObject::new(saved.clone(), file_based) + } +} + /// Bring `store` to `wanted`, keeping each object whose connection is /// unchanged where it can. -fn patch(store: &gio::ListStore, wanted: &[SavedConnection]) { +fn patch( + store: &gio::ListStore, + wanted: &[SavedConnection], + object: impl Fn(&SavedConnection) -> SavedConnectionObject, +) { let mut position = 0; for saved in wanted { let kept = (position..store.n_items()).find(|&at| { @@ -148,7 +165,7 @@ fn patch(store: &gio::ListStore, wanted: &[SavedConnection]) { match kept { Some(at) if at > position => store.splice(position, at - position, &[] as &[SavedConnectionObject]), Some(_) => {} - None => store.insert(position, &SavedConnectionObject::new(saved.clone())), + None => store.insert(position, &object(saved)), } position += 1; } @@ -223,7 +240,7 @@ mod tests { fn a_write_to_the_store_reaches_the_list() { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, Arc::new(DriverRegistry::new())); assert!(matches!(list.state(), ConnectionListState::Loading)); let corp = connection("Corp"); @@ -240,7 +257,7 @@ mod tests { fn unreadable_list_keeps_rows_and_reports_its_state() { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, Arc::new(DriverRegistry::new())); let seen = Rc::new(RefCell::new(Vec::new())); let sink = seen.clone(); let observer = list.connect_state_changed(move |state| { @@ -270,7 +287,7 @@ mod tests { fn a_write_keeps_the_objects_of_unchanged_connections() { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, Arc::new(DriverRegistry::new())); let (alpha, beta, gamma) = (connection("Alpha"), connection("Beta"), connection("Gamma")); list.apply(ready(&[alpha.clone(), beta.clone(), gamma.clone()])); let before = objects(&list); @@ -293,7 +310,7 @@ mod tests { fn a_new_group_replaces_every_object() { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, Arc::new(DriverRegistry::new())); let (alpha, beta) = (connection("Alpha"), connection("Beta")); list.apply(ready(&[alpha.clone(), beta.clone()])); let before = objects(&list); @@ -311,7 +328,7 @@ mod tests { fn a_dropped_list_stops_following() { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, Arc::new(DriverRegistry::new())); let model = list.model(); drop(list); diff --git a/linux/crates/app/src/services/saved_connection_object.rs b/linux/crates/app/src/services/saved_connection_object.rs index c85d92c9ae..21e39cc954 100644 --- a/linux/crates/app/src/services/saved_connection_object.rs +++ b/linux/crates/app/src/services/saved_connection_object.rs @@ -15,9 +15,12 @@ glib::wrapper! { } impl SavedConnectionObject { - pub(crate) fn new(saved: SavedConnection) -> Self { + /// `file_based` says whether the connection's driver opens a file, + /// which decides what the row names: the file, or the server. + pub(crate) fn new(saved: SavedConnection, file_based: bool) -> Self { let object: Self = glib::Object::new(); let _ = object.imp().saved.set(saved); + object.imp().file_based.set(file_based); object } @@ -25,18 +28,19 @@ impl SavedConnectionObject { pub(crate) fn saved(&self) -> Option<&SavedConnection> { self.imp().saved.get() } -} -pub(crate) fn is_sqlite(saved: &SavedConnection) -> bool { - saved.driver_id == "sqlite" + pub(crate) fn file_based(&self) -> bool { + self.imp().file_based.get() + } } -/// The driver and where it connects. `host_path` names the file a -/// portal path stands for, once it is read. -pub(crate) fn subtitle_for(saved: &SavedConnection, host_path: Option<&Path>) -> String { - if is_sqlite(saved) { +/// The driver and where it connects: the file for a driver that opens +/// one, the server otherwise. `host_path` names the file a portal path +/// stands for, once it is read. +pub(crate) fn subtitle_for(saved: &SavedConnection, file_based: bool, host_path: Option<&Path>) -> String { + if file_based { let shown = file_location::display_path(Path::new(&saved.database), host_path); - return format!("sqlite · {shown}"); + return format!("{} · {shown}", saved.driver_id); } match saved.auth_mode { AuthMode::Kerberos => format!("{} · {}:{}", saved.driver_id, saved.host, saved.port), @@ -73,11 +77,11 @@ mod tests { #[test] fn a_kerberos_row_has_no_username_separator_to_dangle() { assert_eq!( - subtitle_for(&saved("", AuthMode::Kerberos), None), + subtitle_for(&saved("", AuthMode::Kerberos), false, None), "mssql · sql.corp.example:1433" ); assert_eq!( - subtitle_for(&saved("sa", AuthMode::Password), None), + subtitle_for(&saved("sa", AuthMode::Password), false, None), "mssql · sa@sql.corp.example:1433" ); } @@ -89,12 +93,25 @@ mod tests { file.database = "/run/user/1000/doc/1a2b3c/app.db".into(); assert_eq!( - subtitle_for(&file, Some(Path::new("/srv/data/app.db"))), + subtitle_for(&file, true, Some(Path::new("/srv/data/app.db"))), "sqlite · /srv/data/app.db" ); - assert_eq!(subtitle_for(&file, None), "sqlite · app.db"); + assert_eq!(subtitle_for(&file, true, None), "sqlite · app.db"); file.database = "/home/me/app.db".into(); - assert_eq!(subtitle_for(&file, None), "sqlite · /home/me/app.db"); + assert_eq!(subtitle_for(&file, true, None), "sqlite · /home/me/app.db"); + } + + /// Any engine that opens a file names the file, not a server it + /// does not have. + #[test] + fn every_file_engine_names_its_file() { + let mut file = saved("", AuthMode::Password); + file.driver_id = "duckdb".into(); + file.host = String::new(); + file.port = 0; + file.database = "/home/me/sales.duckdb".into(); + + assert_eq!(subtitle_for(&file, true, None), "duckdb · /home/me/sales.duckdb"); } #[gtk4::test] @@ -103,7 +120,7 @@ mod tests { connection.group = Some(tablepro_storage::ConnectionGroupName::new("Work").expect("a group name")); connection.last_opened_at = Utc.timestamp_opt(1_700_000_000, 0).single(); - let object = SavedConnectionObject::new(connection.clone()); + let object = SavedConnectionObject::new(connection.clone(), false); assert_eq!(object.id(), connection.id.to_string()); assert_eq!(object.name(), "Corp"); diff --git a/linux/crates/app/src/services/saved_connection_object_imp.rs b/linux/crates/app/src/services/saved_connection_object_imp.rs index 97bf17f6bc..70ff827cbd 100644 --- a/linux/crates/app/src/services/saved_connection_object_imp.rs +++ b/linux/crates/app/src/services/saved_connection_object_imp.rs @@ -1,4 +1,4 @@ -use std::cell::OnceCell; +use std::cell::{Cell, OnceCell}; use std::marker::PhantomData; use gtk4::glib; @@ -10,6 +10,7 @@ use tablepro_storage::SavedConnection; #[properties(wrapper_type = super::saved_connection_object::SavedConnectionObject)] pub struct SavedConnectionObject { pub(super) saved: OnceCell, + pub(super) file_based: Cell, #[property(get = Self::id)] id: PhantomData, #[property(get = Self::name)] @@ -45,7 +46,7 @@ impl SavedConnectionObject { fn subtitle(&self) -> String { self.saved .get() - .map(|saved| super::saved_connection_object::subtitle_for(saved, None)) + .map(|saved| super::saved_connection_object::subtitle_for(saved, self.file_based.get(), None)) .unwrap_or_default() } diff --git a/linux/crates/app/src/ui/connect_dialog/sqlite_file_row.rs b/linux/crates/app/src/ui/connect_dialog/database_file_row.rs similarity index 65% rename from linux/crates/app/src/ui/connect_dialog/sqlite_file_row.rs rename to linux/crates/app/src/ui/connect_dialog/database_file_row.rs index 998e122b26..b5ec3c6405 100644 --- a/linux/crates/app/src/ui/connect_dialog/sqlite_file_row.rs +++ b/linux/crates/app/src/ui/connect_dialog/database_file_row.rs @@ -5,7 +5,7 @@ use gtk4::prelude::*; use gtk4::subclass::prelude::*; use libadwaita as adw; use libadwaita::prelude::*; -use tablepro_core::{FileEndpoint, FileOpenMode}; +use tablepro_core::{DatabaseDriver, FileEndpoint, FileFormat, FileOpenMode}; use tablepro_session::runtime::Tasks; use crate::services::file_location; @@ -20,21 +20,23 @@ mod imp { use super::*; #[derive(Default, gtk4::CompositeTemplate)] - #[template(resource = "/app/tablepro/TablePro/ui/sqlite-file-row.ui")] - pub struct SqliteFileRow { + #[template(resource = "/app/tablepro/TablePro/ui/database-file-row.ui")] + pub struct DatabaseFileRow { #[template_child] pub choose_button: TemplateChild, #[template_child] pub create_button: TemplateChild, pub endpoint: RefCell>, + /// The engine's files, which the choosers offer first. + pub files: RefCell>, /// Reads the file a portal path stands for, off the GTK thread. pub tasks: OnceCell, } #[glib::object_subclass] - impl ObjectSubclass for SqliteFileRow { - const NAME: &'static str = "TableProSqliteFileRow"; - type Type = super::SqliteFileRow; + impl ObjectSubclass for DatabaseFileRow { + const NAME: &'static str = "TableProDatabaseFileRow"; + type Type = super::DatabaseFileRow; type ParentType = adw::ActionRow; fn class_init(klass: &mut Self::Class) { @@ -46,7 +48,7 @@ mod imp { } } - impl ObjectImpl for SqliteFileRow { + impl ObjectImpl for DatabaseFileRow { fn signals() -> &'static [Signal] { static SIGNALS: OnceLock> = OnceLock::new(); SIGNALS.get_or_init(|| vec![Signal::builder("changed").build()]) @@ -70,30 +72,36 @@ mod imp { } } - impl WidgetImpl for SqliteFileRow {} - impl ListBoxRowImpl for SqliteFileRow {} - impl PreferencesRowImpl for SqliteFileRow {} - impl ActionRowImpl for SqliteFileRow {} + impl WidgetImpl for DatabaseFileRow {} + impl ListBoxRowImpl for DatabaseFileRow {} + impl PreferencesRowImpl for DatabaseFileRow {} + impl ActionRowImpl for DatabaseFileRow {} } glib::wrapper! { - /// The SQLite database file, opened as it is or created on connect. - pub struct SqliteFileRow(ObjectSubclass) + /// A file engine's database file, opened as it is or created on + /// connect. + pub struct DatabaseFileRow(ObjectSubclass) @extends adw::ActionRow, adw::PreferencesRow, gtk4::ListBoxRow, gtk4::Widget, @implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget; } -impl Default for SqliteFileRow { +impl Default for DatabaseFileRow { fn default() -> Self { glib::Object::new() } } -impl SqliteFileRow { +impl DatabaseFileRow { pub fn endpoint(&self) -> Option { self.imp().endpoint.borrow().clone() } + /// Which engine's files the choosers offer. + pub fn set_files(&self, files: Option) { + self.imp().files.replace(files); + } + pub fn set_tasks(&self, tasks: Tasks) { let _ = self.imp().tasks.set(tasks); } @@ -153,7 +161,8 @@ impl SqliteFileRow { } async fn choose_existing(&self) { - let dialog = sqlite_file_dialog(&crate::i18n::gettext("Open Database File")); + let files = self.imp().files.borrow().clone(); + let dialog = database_file_dialog(&crate::i18n::gettext("Open Database File"), files.as_ref()); let chosen = dialog.open_future(self.parent_window().as_ref()).await; if let Some(path) = chosen.ok().and_then(|file| file.path()) { self.adopt_choice(path, FileOpenMode::OpenExisting); @@ -163,8 +172,9 @@ impl SqliteFileRow { /// The save chooser asks before replacing a file that exists, and /// the driver then replaces it whole, so no second question here. async fn choose_new(&self) { - let dialog = sqlite_file_dialog(&crate::i18n::gettext("New Database File")); - dialog.set_initial_name(Some("Untitled.sqlite3")); + let files = self.imp().files.borrow().clone(); + let dialog = database_file_dialog(&crate::i18n::gettext("New Database File"), files.as_ref()); + dialog.set_initial_name(Some(&new_file_name(files.as_ref()))); let chosen = dialog.save_future(self.parent_window().as_ref()).await; if let Some(path) = chosen.ok().and_then(|file| file.path()) { self.adopt_choice(path, FileOpenMode::CreateNew); @@ -189,35 +199,85 @@ fn subtitle(endpoint: Option<&FileEndpoint>, host_path: Option<&Path>) -> String } } -/// The file chooser for a SQLite database, with every file a choice -/// when the filter misses it. -pub(crate) fn sqlite_file_dialog(title: &str) -> gtk4::FileDialog { - let sqlite = gtk4::FileFilter::new(); - sqlite.set_name(Some(&crate::i18n::gettext("SQLite databases"))); - for mime in ["application/vnd.sqlite3", "application/x-sqlite3"] { - sqlite.add_mime_type(mime); +/// The files one engine opens: its name, for the chooser's filter, and +/// the suffixes and types its files go by. +#[derive(Debug, Clone, PartialEq)] +pub struct DatabaseFiles { + pub engine: String, + pub format: FileFormat, +} + +impl DatabaseFiles { + /// The files `driver` opens, for a driver that opens one. + pub(crate) fn of(driver: &dyn DatabaseDriver) -> Option { + driver.capabilities().file_format().map(|format| Self { + engine: driver.display_name().to_owned(), + format: *format, + }) } - for suffix in ["sqlite", "sqlite3", "db", "db3"] { - sqlite.add_suffix(suffix); +} + +/// The name a new database is offered under, with the engine's suffix. +fn new_file_name(files: Option<&DatabaseFiles>) -> String { + match files.and_then(|files| files.format.new_file_suffix()) { + Some(suffix) => format!("Untitled.{suffix}"), + None => "Untitled".to_owned(), } +} + +/// The file chooser for an engine's databases, with every file a choice +/// when the filter misses it. +pub(crate) fn database_file_dialog(title: &str, files: Option<&DatabaseFiles>) -> gtk4::FileDialog { let any = gtk4::FileFilter::new(); any.set_name(Some(&crate::i18n::gettext("All files"))); any.add_pattern("*"); let filters = gio::ListStore::new::(); - filters.append(&sqlite); + let builder = gtk4::FileDialog::builder().title(title).modal(true); + let Some(files) = files else { + filters.append(&any); + return builder.filters(&filters).default_filter(&any).build(); + }; + let engine = gtk4::FileFilter::new(); + engine.set_name(Some(&crate::i18n::gettext_f( + "{engine} databases", + &[("engine", &files.engine)], + ))); + for mime in files.format.mime_types { + engine.add_mime_type(mime); + } + for suffix in files.format.suffixes { + engine.add_suffix(suffix); + } + filters.append(&engine); filters.append(&any); - gtk4::FileDialog::builder() - .title(title) - .modal(true) - .filters(&filters) - .default_filter(&sqlite) - .build() + builder.filters(&filters).default_filter(&engine).build() } #[cfg(test)] mod tests { + use tablepro_core::FileSignature; + use super::*; + #[test] + fn a_new_database_is_offered_with_its_engines_suffix() { + let files = DatabaseFiles { + engine: "DuckDB".to_owned(), + format: FileFormat { + signature: FileSignature { + offset: 8, + bytes: b"DUCK", + }, + suffixes: &["duckdb", "ddb"], + mime_types: &[], + empty_file_is_database: false, + }, + }; + + assert_eq!(new_file_name(Some(&files)), "Untitled.duckdb"); + assert_eq!(new_file_name(None), "Untitled"); + } + #[test] fn a_portal_file_reads_as_its_host_path_or_its_name() { let portal = FileEndpoint::open_existing(PathBuf::from("/run/user/1000/doc/1a2b3c/app.db")).expect("absolute"); diff --git a/linux/crates/app/src/ui/connect_dialog/imp.rs b/linux/crates/app/src/ui/connect_dialog/imp.rs index 65c301e18f..9381260b52 100644 --- a/linux/crates/app/src/ui/connect_dialog/imp.rs +++ b/linux/crates/app/src/ui/connect_dialog/imp.rs @@ -12,8 +12,8 @@ use tablepro_session::ConnectAttempts; use super::connect_dialog_mode::ConnectDialogMode; use super::connect_submission::ConnectSubmission; +use super::database_file_row::DatabaseFileRow; use super::pem_validation::PemValidation; -use super::sqlite_file_row::SqliteFileRow; use super::tls_section::TlsSection; use super::{DialogServices, DriverEntry}; use crate::ui::ssh_section::SshSection; @@ -40,7 +40,7 @@ pub struct ConnectDialog { #[template_child] pub database_row: TemplateChild, #[template_child] - pub file_row: TemplateChild, + pub file_row: TemplateChild, #[template_child] pub auth_group: TemplateChild, #[template_child] @@ -99,7 +99,7 @@ impl ObjectSubclass for ConnectDialog { type ParentType = adw::Dialog; fn class_init(klass: &mut Self::Class) { - SqliteFileRow::ensure_type(); + DatabaseFileRow::ensure_type(); TlsSection::ensure_type(); SshSection::ensure_type(); klass.bind_template(); diff --git a/linux/crates/app/src/ui/connect_dialog/mod.rs b/linux/crates/app/src/ui/connect_dialog/mod.rs index 13279b2344..8c6716a9db 100644 --- a/linux/crates/app/src/ui/connect_dialog/mod.rs +++ b/linux/crates/app/src/ui/connect_dialog/mod.rs @@ -5,12 +5,12 @@ mod connect_dialog_mode; mod connect_form; mod connect_submission; +mod database_file_row; mod form_errors; mod form_field; mod imp; mod pem_validation; mod row_problem; -mod sqlite_file_row; #[cfg(test)] mod tests; mod tls_section; @@ -38,11 +38,11 @@ pub use self::connect_dialog_mode::ConnectDialogMode; use self::connect_form::ValidatedForm; pub use self::connect_form::{ConnectForm, driver_shows_server_name}; pub use self::connect_submission::ConnectSubmission; +pub(crate) use self::database_file_row::{DatabaseFiles, database_file_dialog}; pub use self::form_errors::FormErrors; pub use self::form_field::FormField; use self::pem_validation::{PemState, PemValidation}; pub use self::row_problem::show_problem; -pub(crate) use self::sqlite_file_row::sqlite_file_dialog; use crate::services::saved_connection_editor::SavedConnectionEditor; use crate::services::secret_changes::SecretChanges; use crate::services::session_context::CredentialSourceFactory; @@ -456,6 +456,7 @@ impl ConnectDialog { crate::i18n::gettext("Database (optional)") }); imp.file_row.set_visible(file_based); + imp.file_row.set_files(DatabaseFiles::of(entry.driver.as_ref())); imp.auth_group.set_visible(!file_based && !caps.auth_modes.is_empty()); imp.ssh_section.set_visible(!file_based); let tls = if file_based { None } else { caps.tls.as_ref() }; @@ -864,7 +865,12 @@ pub fn remedy_field(remedy: TlsRemedy) -> FormField { /// Capabilities for a registry with no drivers at all, so the form /// still has something to read. static NO_DRIVER: DriverCapabilities = DriverCapabilities { - endpoint: tablepro_core::EndpointKind::File, + endpoint: tablepro_core::EndpointKind::File(tablepro_core::FileFormat { + signature: tablepro_core::FileSignature { offset: 0, bytes: &[] }, + suffixes: &[], + mime_types: &[], + empty_file_is_database: false, + }), auth_modes: &[], tls: None, read_only: tablepro_core::ReadOnlyEnforcement::LoginPrivileges, diff --git a/linux/crates/app/src/ui/connect_dialog/tests.rs b/linux/crates/app/src/ui/connect_dialog/tests.rs index bcc929c9a5..00cec49658 100644 --- a/linux/crates/app/src/ui/connect_dialog/tests.rs +++ b/linux/crates/app/src/ui/connect_dialog/tests.rs @@ -48,6 +48,7 @@ impl Default for Options { fn registry() -> Arc { let mut registry = DriverRegistry::new(); registry.register(Arc::new(tablepro_driver_clickhouse::ClickhouseDriver)); + registry.register(Arc::new(tablepro_driver_duckdb::DuckDbDriver)); registry.register(Arc::new(tablepro_driver_mssql::MssqlDriver)); registry.register(Arc::new(tablepro_driver_mysql::MysqlDriver)); registry.register(Arc::new(tablepro_driver_postgres::PgDriver)); diff --git a/linux/crates/app/src/ui/error_text/driver.rs b/linux/crates/app/src/ui/error_text/driver.rs index a252bacc5f..e683c999d4 100644 --- a/linux/crates/app/src/ui/error_text/driver.rs +++ b/linux/crates/app/src/ui/error_text/driver.rs @@ -179,6 +179,7 @@ fn server_code_text(code: &ServerCode) -> String { ServerCode::Tds { .. } => crate::i18n::gettext_f("SQL Server error {code}", &[("code", &number)]), ServerCode::ClickHouse { .. } => crate::i18n::gettext_f("ClickHouse code {code}", &[("code", &number)]), ServerCode::Sqlite { .. } => crate::i18n::gettext_f("SQLite error {code}", &[("code", &number)]), + ServerCode::DuckDb { .. } => crate::i18n::gettext_f("DuckDB {code} error", &[("code", &number)]), } } diff --git a/linux/crates/app/src/ui/picker/connection_row.rs b/linux/crates/app/src/ui/picker/connection_row.rs index 65fdf39156..515d317222 100644 --- a/linux/crates/app/src/ui/picker/connection_row.rs +++ b/linux/crates/app/src/ui/picker/connection_row.rs @@ -13,7 +13,7 @@ use super::connection_menu::{ }; use crate::application::{ShortcutOwner, local_shortcut_controller}; use crate::services::file_location; -use crate::services::saved_connection_object::{SavedConnectionObject, is_sqlite, subtitle_for}; +use crate::services::saved_connection_object::{SavedConnectionObject, subtitle_for}; use crate::ui::a11y::describe_icon_button; /// The row for one saved connection: activating it connects, and its @@ -25,16 +25,16 @@ pub(crate) fn connection_row( groups: &[ConnectionGroupName], tasks: Option<&Tasks>, ) -> gtk::Widget { - let Some(saved) = object - .downcast_ref::() - .and_then(SavedConnectionObject::saved) - .cloned() - else { + let Some(connection) = object.downcast_ref::() else { return adw::ActionRow::new().upcast(); }; + let Some(saved) = connection.saved().cloned() else { + return adw::ActionRow::new().upcast(); + }; + let file_based = connection.file_based(); let row = adw::ActionRow::builder() .title(&saved.name) - .subtitle(subtitle_for(&saved, None)) + .subtitle(subtitle_for(&saved, file_based, None)) // A name is the user's text, never markup. .use_markup(false) .activatable(true) @@ -60,7 +60,7 @@ pub(crate) fn connection_row( row.add_controller(local_shortcut_controller(ShortcutOwner::PickerRow)); row.add_controller(secondary_click(&menu_button)); if let Some(tasks) = tasks { - show_host_path(&row, &saved, tasks); + show_host_path(&row, &saved, file_based, tasks); } row.upcast() } @@ -148,16 +148,16 @@ pub(super) fn tag_classes(color: Option) -> Vec<&'static str> { /// A portal path's folder is an id, so the row names the file the /// portal stands for once it is read. -fn show_host_path(row: &adw::ActionRow, saved: &SavedConnection, tasks: &Tasks) { +fn show_host_path(row: &adw::ActionRow, saved: &SavedConnection, file_based: bool, tasks: &Tasks) { let path = PathBuf::from(&saved.database); - if !is_sqlite(saved) || !file_location::is_portal_path(&path) { + if !file_based || !file_location::is_portal_path(&path) { return; } let looking = tasks.spawn_blocking_task(move || file_location::portal_host_path_blocking(&path)); let (saved, row) = (saved.clone(), row.downgrade()); glib::spawn_future_local(async move { if let (Ok(Some(host)), Some(row)) = (looking.await, row.upgrade()) { - row.set_subtitle(&subtitle_for(&saved, Some(&host))); + row.set_subtitle(&subtitle_for(&saved, true, Some(&host))); } }); } diff --git a/linux/crates/app/src/ui/picker/tests.rs b/linux/crates/app/src/ui/picker/tests.rs index f7a2542c0c..76535320d8 100644 --- a/linux/crates/app/src/ui/picker/tests.rs +++ b/linux/crates/app/src/ui/picker/tests.rs @@ -55,7 +55,7 @@ impl Pickers { crate::register_test_resources(); let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - let list = SavedConnectionList::follow(&store); + let list = SavedConnectionList::follow(&store, std::sync::Arc::new(crate::build_registry())); let views = [ WelcomeView::new(list.clone(), None), WelcomeView::new(list.clone(), None), diff --git a/linux/crates/app/src/ui/session_window/sqlite_location_dialog.rs b/linux/crates/app/src/ui/session_window/file_location_dialog.rs similarity index 97% rename from linux/crates/app/src/ui/session_window/sqlite_location_dialog.rs rename to linux/crates/app/src/ui/session_window/file_location_dialog.rs index 02e5ca2399..0211cf1b93 100644 --- a/linux/crates/app/src/ui/session_window/sqlite_location_dialog.rs +++ b/linux/crates/app/src/ui/session_window/file_location_dialog.rs @@ -1,4 +1,4 @@ -//! The question asked before a SQLite file on a network or FUSE +//! The question asked before a database file on a network or FUSE //! filesystem opens read-write. use std::path::PathBuf; @@ -38,7 +38,7 @@ pub(crate) enum LocationChoice { #[derive(Debug)] pub(crate) enum FileCheck { /// Read the filesystem `path` is on before connecting, and stop to - /// ask when SQLite's locking cannot be trusted there. + /// ask when the engines' file locks cannot be trusted there. Pending { path: PathBuf, classify: LocationClassifier, @@ -53,7 +53,7 @@ impl FileCheck { } /// Run `connecting` once the file is known to be on a filesystem - /// whose locking SQLite can trust. The check reads the disk on the + /// whose locks the engines can trust. The check reads the disk on the /// spawner, and only a file that needs the question comes back to /// the GTK thread before connecting. pub(crate) fn guard( @@ -94,7 +94,7 @@ fn location_dialog() -> adw::AlertDialog { let dialog = adw::AlertDialog::new( Some(&crate::i18n::gettext("Database on a Network Filesystem")), Some(&crate::i18n::gettext( - "SQLite can corrupt a database on a network or FUSE filesystem when more than one program writes to it.", + "A database on a network or FUSE filesystem can be corrupted when more than one program writes to it.", )), ); dialog.add_responses(&[ diff --git a/linux/crates/app/src/ui/session_window/locate_file.rs b/linux/crates/app/src/ui/session_window/locate_file.rs index 21978df47d..123be2fd3e 100644 --- a/linux/crates/app/src/ui/session_window/locate_file.rs +++ b/linux/crates/app/src/ui/session_window/locate_file.rs @@ -1,4 +1,4 @@ -//! A saved SQLite connection whose file is gone: find the file again, +//! A saved file connection whose file is gone: find the file again, //! save its new path and connect to it. use std::path::{Path, PathBuf}; @@ -20,21 +20,34 @@ use uuid::Uuid; use super::session_page::SessionPage; use super::{SessionWindow, SessionWindowInput}; use crate::runtime::DeliverInput; +use crate::ui::connect_dialog::DatabaseFiles; const CANCEL: &str = "cancel"; const LOCATE: &str = "locate"; /// Where the user says a lost database file is now. pub(crate) trait LocateChooser { - fn choose(&self, parent: >k::Window, missing: &Path) -> LocalBoxFuture<'static, Option>; + fn choose( + &self, + parent: >k::Window, + missing: &Path, + files: Option<&DatabaseFiles>, + ) -> LocalBoxFuture<'static, Option>; } -/// The file chooser, opened where the file used to be. +/// The file chooser, opened where the file used to be and offering the +/// connection's engine's files first. struct FileDialogChooser; impl LocateChooser for FileDialogChooser { - fn choose(&self, parent: >k::Window, missing: &Path) -> LocalBoxFuture<'static, Option> { - let dialog = crate::ui::connect_dialog::sqlite_file_dialog(&crate::i18n::gettext("Locate Database File")); + fn choose( + &self, + parent: >k::Window, + missing: &Path, + files: Option<&DatabaseFiles>, + ) -> LocalBoxFuture<'static, Option> { + let dialog = + crate::ui::connect_dialog::database_file_dialog(&crate::i18n::gettext("Locate Database File"), files); // An initial file is read synchronously, which hangs on a dead // mount; a folder is only stored. if let Some(folder) = missing.parent() { @@ -76,6 +89,7 @@ impl SessionWindow { /// Connection. pub(super) fn offer_locate(&self, id: Uuid, missing: PathBuf) { let dialog = not_found_dialog(&missing); + let files = self.located_files(id); let window = self.downgrade(); glib::spawn_future_local(async move { let Some(window) = window.upgrade() else { @@ -90,13 +104,26 @@ impl SessionWindow { .borrow() .clone() .unwrap_or_else(|| Rc::new(FileDialogChooser)); - let chosen = chooser.choose(window.upcast_ref(), &missing).await; + let chosen = chooser.choose(window.upcast_ref(), &missing, files.as_ref()).await; if let Some(path) = chosen { window.send(SessionWindowInput::FileLocated { id, path }); } }); } + /// The files the failed connection's engine opens. + fn located_files(&self, id: Uuid) -> Option { + let driver_id = match &*self.imp().page.borrow() { + SessionPage::ConnectFailed { + source: ConnectSource::Saved(saved), + .. + } if saved.id == id => saved.driver_id.clone(), + _ => return None, + }; + let driver = self.state()?.context.drivers.get(&driver_id)?; + DatabaseFiles::of(driver.as_ref()) + } + /// Save the file's new path on the connection the failed page is /// about, then connect to it once the write lands, so the entry and /// the open session name the same file. @@ -181,7 +208,12 @@ mod tests { struct ChosenFile(PathBuf); impl LocateChooser for ChosenFile { - fn choose(&self, _parent: >k::Window, _missing: &Path) -> LocalBoxFuture<'static, Option> { + fn choose( + &self, + _parent: >k::Window, + _missing: &Path, + _files: Option<&DatabaseFiles>, + ) -> LocalBoxFuture<'static, Option> { let path = self.0.clone(); async move { Some(path) }.boxed_local() } diff --git a/linux/crates/app/src/ui/session_window/mod.rs b/linux/crates/app/src/ui/session_window/mod.rs index 0e01ce017d..c258a94620 100644 --- a/linux/crates/app/src/ui/session_window/mod.rs +++ b/linux/crates/app/src/ui/session_window/mod.rs @@ -10,6 +10,7 @@ mod connect_flow; mod connecting_page; mod connections; mod crash_notice; +mod file_location_dialog; mod imp; mod locate_file; mod notification_request; @@ -23,7 +24,6 @@ mod session_restore; mod session_teardown; mod session_window_command; mod session_window_input; -mod sqlite_location_dialog; #[cfg(test)] pub(crate) mod tests; mod window_actions; @@ -53,12 +53,12 @@ use tablepro_storage::{CredentialDemand, SavedConnection}; pub(super) use self::connect_failed_page::ConnectFailedPage; use self::connect_failure_view::ConnectFailureView; pub(super) use self::connecting_page::ConnectingPage; +use self::file_location_dialog::FileCheck; pub(super) use self::picker_page::PickerPage; use self::resolve_file_identity::{FileResolveError, FileTarget, ResolvedFile, resolve_file_blocking}; use self::session_page::SessionPage; use self::session_window_command::{AttemptError, ConnectFailure, Connected, SessionWindowCommand}; pub(crate) use self::session_window_input::SessionWindowInput; -use self::sqlite_location_dialog::FileCheck; use crate::application::{AppServices, WindowHost, WindowLaunch}; use crate::runtime::{DeliverInput, InputSender}; use crate::services::session_context::SessionContext; @@ -748,7 +748,7 @@ impl SessionWindow { } /// `answer` is how the user chose to open a database file on a - /// filesystem whose locking SQLite cannot trust, once asked. + /// filesystem whose locking the engines cannot trust, once asked. fn start_connect( &self, claim: SessionClaim, @@ -1127,9 +1127,25 @@ fn connect_file( ) -> BoxFuture<'static, Result> { let connector = context.connector.clone(); let interaction = context.attended(); - let request = resolve_file_identity::file_request(identity, &context.drivers, read_only); + let drivers = context.drivers.clone(); + let identity = identity.clone(); + let path = resolve_file_identity::file_path(&identity); + let header_len = drivers.file_header_len(); + // Which engine opens the file is in its first bytes, read off the + // GTK thread. + let reading = context + .tasks + .spawn_blocking_task(move || resolve_file_identity::read_header_blocking(&path, header_len)); async move { - let request = request.map_err(|error| attempt_error(&error, false))?; + let header = cancel + .run_until_cancelled(reading) + .await + .ok_or(AttemptError::Cancelled)? + .map_err(|failure| DriverError::Protocol(format!("reading the file stopped: {failure}"))) + .and_then(|read| read) + .map_err(|error| attempt_error(&ConnectError::Driver(error), false))?; + let request = resolve_file_identity::file_request(&identity, &drivers, &header, read_only) + .map_err(|error| attempt_error(&error, false))?; connect_flow::connect_request(&*connector, request, interaction, cancel) .await .map(unsaved) diff --git a/linux/crates/app/src/ui/session_window/resolve_file_identity.rs b/linux/crates/app/src/ui/session_window/resolve_file_identity.rs index 292db164dc..0f1315169d 100644 --- a/linux/crates/app/src/ui/session_window/resolve_file_identity.rs +++ b/linux/crates/app/src/ui/session_window/resolve_file_identity.rs @@ -2,26 +2,35 @@ //! resolved off the GTK thread. use std::path::PathBuf; -use std::sync::Arc; -use tablepro_core::{DatabaseDriver, DriverError, DriverRegistry, FileEndpoint, SafeModeLevel, WorkspaceSettings}; +use tablepro_core::{DriverError, DriverRegistry, FileEndpoint, SafeModeLevel, WorkspaceSettings}; use tablepro_session::registry::FileIdentity; use tablepro_session::{ConnectError, ConnectRequest, ConnectionMetadata, RequestTarget}; -pub(crate) use crate::services::file_resolution::{FileResolveError, FileTarget, ResolvedFile, resolve_file_blocking}; +pub(crate) use crate::services::file_resolution::{ + FileResolveError, FileTarget, ResolvedFile, read_header_blocking, resolve_file_blocking, +}; -/// The request that opens a file nobody saved, through the driver that -/// opens files. +/// The path a handed-over file opens at. +pub(crate) fn file_path(identity: &FileIdentity) -> PathBuf { + identity + .canonical_path + .clone() + .unwrap_or_else(|| PathBuf::from(&identity.uri)) +} + +/// The request that opens a file nobody saved, through the engine whose +/// files carry `header`, the file's first bytes, or go by its name. pub(crate) fn file_request( identity: &FileIdentity, drivers: &DriverRegistry, + header: &[u8], read_only: bool, ) -> Result { - let driver = file_driver(drivers).ok_or_else(|| ConnectError::UnknownDriver("sqlite".to_owned()))?; - let path = identity - .canonical_path - .clone() - .unwrap_or_else(|| PathBuf::from(&identity.uri)); + let path = file_path(identity); + let driver = drivers + .file_driver_for(header, &path) + .ok_or_else(|| DriverError::NotADatabase { path: path.clone() })?; let name = path .file_name() .map_or_else(|| identity.uri.clone(), |name| name.to_string_lossy().into_owned()); @@ -43,20 +52,9 @@ pub(crate) fn file_request( Ok(request) } -fn file_driver(drivers: &DriverRegistry) -> Option> { - drivers - .get("sqlite") - .filter(|driver| driver.capabilities().is_file_based()) - .or_else(|| { - drivers - .iter() - .find(|driver| driver.capabilities().is_file_based()) - .cloned() - }) -} - #[cfg(test)] mod tests { + use std::sync::Arc; use std::time::Duration; use super::*; @@ -99,14 +97,47 @@ mod tests { ))); let identity = FileIdentity::new(Some(PathBuf::from("/srv/app.db")), "file:///srv/app.db".to_owned()); - let request = file_request(&identity, &drivers, false).expect("a request"); - let read_only = file_request(&identity, &drivers, true).expect("a read-only request"); + let request = file_request(&identity, &drivers, b"", false).expect("a request"); + let read_only = file_request(&identity, &drivers, b"", true).expect("a read-only request"); assert_eq!(request.metadata.name, "app.db"); assert!(matches!(request.target, RequestTarget::File(_))); assert!(!request.workspace_settings.read_only); assert!(read_only.workspace_settings.read_only); assert_eq!(read_only.safe_mode, SafeModeLevel::ReadOnly); - assert!(file_request(&identity, &DriverRegistry::new(), false).is_err()); + assert!(file_request(&identity, &DriverRegistry::new(), b"", false).is_err()); + } + + /// The file's own bytes pick its engine, whatever it is named: a + /// DuckDB file called `.db` is no SQLite file. + #[test] + fn the_engine_whose_signature_a_file_carries_opens_it() { + let mut drivers = DriverRegistry::new(); + for (id, capabilities) in [ + ("sqlite", &tablepro_test_support::FakeDriver::FILE_CAPABILITIES), + ("duckdb", &tablepro_test_support::FakeDriver::SIGNED_FILE_CAPABILITIES), + ] { + drivers.register(Arc::new( + tablepro_test_support::FakeDriver::connecting( + capabilities, + tablepro_test_support::ConnectionScript::default(), + ) + .with_id(id), + )); + } + let identity = FileIdentity::new(Some(PathBuf::from("/srv/sales.db")), "file:///srv/sales.db".to_owned()); + let mut header = vec![0u8; 16]; + header[8..12].copy_from_slice(b"DUCK"); + + let duck = file_request(&identity, &drivers, &header, false).expect("a request"); + let plain = file_request(&identity, &drivers, b"", false).expect("a request"); + let unknown = file_request(&identity, &drivers, b"some text file", false); + + assert_eq!(duck.metadata.driver_id, "duckdb"); + assert_eq!(plain.metadata.driver_id, "sqlite", "an empty .db file goes by its name"); + assert!( + matches!(unknown, Err(ConnectError::Driver(DriverError::NotADatabase { .. }))), + "{unknown:?}" + ); } } diff --git a/linux/crates/app/src/ui/session_window/session_window_command.rs b/linux/crates/app/src/ui/session_window/session_window_command.rs index e88e1ff3e9..6b0090101c 100644 --- a/linux/crates/app/src/ui/session_window/session_window_command.rs +++ b/linux/crates/app/src/ui/session_window/session_window_command.rs @@ -34,7 +34,7 @@ pub(crate) struct Connected { pub(crate) enum AttemptError { /// The user stopped it, which needs no error. Cancelled, - /// The database file is on a filesystem whose locking SQLite cannot + /// The database file is on a filesystem whose locking the engines cannot /// trust, so the user is asked how to open it first. AskLocation(crate::services::file_location::FileLocationKind), Failed(ConnectFailure), diff --git a/linux/crates/app/src/ui/session_window/session_window_input.rs b/linux/crates/app/src/ui/session_window/session_window_input.rs index 796605f7c0..5ffad45983 100644 --- a/linux/crates/app/src/ui/session_window/session_window_input.rs +++ b/linux/crates/app/src/ui/session_window/session_window_input.rs @@ -4,9 +4,9 @@ use tablepro_session::SessionKey; use tablepro_storage::{ConnectionColor, ConnectionGroupName, SavedConnection}; use uuid::Uuid; +use super::file_location_dialog::LocationChoice; use super::resolve_file_identity::ResolvedFile; use super::session_window_command::SessionWindowCommand; -use super::sqlite_location_dialog::LocationChoice; use crate::services::connection_deletion::DeletionOptions; use crate::ui::connect_dialog::ConnectSubmission; use crate::workspace::{CloseReason, CompletedWork}; @@ -81,7 +81,7 @@ pub(crate) enum SessionWindowInput { /// A notification about the tab was clicked. FocusTab(Uuid), Command(Box), - /// The answer for a file on a filesystem whose locking SQLite cannot + /// The answer for a file on a filesystem whose locking the engines cannot /// trust, or ReadWrite when there was nothing to ask. LocationChosen { generation: tablepro_session::connect::AttemptGeneration, diff --git a/linux/crates/app/src/ui/session_window/tests/mod.rs b/linux/crates/app/src/ui/session_window/tests/mod.rs index 2cfccd433f..40672c35b5 100644 --- a/linux/crates/app/src/ui/session_window/tests/mod.rs +++ b/linux/crates/app/src/ui/session_window/tests/mod.rs @@ -127,6 +127,13 @@ impl TestWindow { &FakeDriver::FILE_CAPABILITIES }; drivers.register(Arc::new(FakeDriver::connecting(capabilities, connection.clone()))); + // A second file engine, as the app ships two, whose files a + // handed-over file is told apart from by its header. + if !network { + drivers.register(Arc::new( + FakeDriver::connecting(&FakeDriver::SIGNED_FILE_CAPABILITIES, connection.clone()).with_id("duckdb"), + )); + } let connector = FakeConnector::new(establish).always(FakeEstablish::Connect(connection)); let liveness = LivenessPolicy::DESKTOP; let hold = Rc::new(CountingHold::new()); diff --git a/linux/crates/app/src/ui/session_window/tests/open_file.rs b/linux/crates/app/src/ui/session_window/tests/open_file.rs index e4232ae32d..1ce469a1ef 100644 --- a/linux/crates/app/src/ui/session_window/tests/open_file.rs +++ b/linux/crates/app/src/ui/session_window/tests/open_file.rs @@ -15,6 +15,18 @@ fn database(dir: &tempfile::TempDir, name: &str) -> gio::File { file } +/// A file carrying the second file engine's signature, under a name the +/// first engine claims. +fn signed_database(dir: &tempfile::TempDir, name: &str) -> gio::File { + let mut header = vec![0u8; 64]; + let signature = tablepro_test_support::FakeDriver::SIGNED_FILE_SIGNATURE; + header[signature.offset..signature.header_len()].copy_from_slice(signature.bytes); + let file = gio::File::for_path(dir.path().join(name)); + file.replace_contents(&header, None, false, gio::FileCreateFlags::NONE, gio::Cancellable::NONE) + .expect("seed the database file"); + file +} + fn other_windows(test: &TestWindow) -> Vec { test.app .windows() @@ -51,6 +63,22 @@ fn an_opened_file_goes_to_the_idle_window_then_its_owner_then_a_new_window() { assert_eq!(test.app.windows().len(), 2); } +/// Files hands over a file by its path alone, and the file's own header +/// says which engine opens it, whatever its name. +#[gtk4::test] +fn an_opened_file_goes_to_the_engine_whose_file_it_is() { + let test = TestWindow::picker("OpenFileEngine"); + let dir = tempfile::tempdir().expect("tempdir"); + let file = signed_database(&dir, "sales.db"); + + test.app.open(std::slice::from_ref(&file), ""); + test.wait_for_page("workspace"); + + let calls = test.connector.calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].request.metadata.driver_id, "duckdb"); +} + #[gtk4::test] fn a_window_opened_for_a_file_another_window_has_closes_again() { let test = TestWindow::picker("OpenFileTwice"); diff --git a/linux/crates/app/src/ui/structure_tab/context.rs b/linux/crates/app/src/ui/structure_tab/context.rs index 6fefa96435..d66928cb6f 100644 --- a/linux/crates/app/src/ui/structure_tab/context.rs +++ b/linux/crates/app/src/ui/structure_tab/context.rs @@ -10,6 +10,7 @@ use tablepro_core::ddl::{ default_text, parse_default, primary_key_permission, }; use tablepro_core::dialect::SqlDialect; +use tablepro_core::meta::IndexInfo; use super::StructureMode; @@ -79,6 +80,18 @@ impl StructureContext { /// A draft's indexes and foreign keys are only entries in it, so the /// engine's limits apply to a table that exists. + /// Whether an index the table had when it was read may be dropped. + /// One added in this tab is only a draft. + pub(super) fn index_removal(&self, index: &IndexInfo) -> Permission { + let (Some(ddl), Some(original)) = (self.ddl(), self.original()) else { + return Permission::Allowed; + }; + match original.indexes.iter().any(|known| known.name == index.name) { + true => ddl.index_removal(original, index), + false => Permission::Allowed, + } + } + pub(super) fn table_permission(&self, allowed: bool) -> Permission { match self.mode { StructureMode::New => Permission::Allowed, diff --git a/linux/crates/app/src/ui/structure_tab/index_dialog.rs b/linux/crates/app/src/ui/structure_tab/index_dialog.rs index 54df15c48b..d39a40ba3b 100644 --- a/linux/crates/app/src/ui/structure_tab/index_dialog.rs +++ b/linux/crates/app/src/ui/structure_tab/index_dialog.rs @@ -35,6 +35,8 @@ struct Inner { columns: Vec, /// The names the table's indexes already take. existing: Vec, + /// Whether the engine keeps a key in descending order. + descending: bool, draft: RefCell, /// An empty name is a problem to point out only once the user has /// typed one. @@ -44,7 +46,12 @@ struct Inner { } impl IndexDialog { - pub(super) fn new(columns: Vec, existing: Vec, on_add: impl Fn(IndexInfo) + 'static) -> Self { + pub(super) fn new( + columns: Vec, + existing: Vec, + descending: bool, + on_add: impl Fn(IndexInfo) + 'static, + ) -> Self { let (form, dialog) = FormDialog::new(&crate::i18n::gettext("Add Index"), &crate::i18n::gettext("_Add")); let details = adw::PreferencesGroup::new(); let name_row = FormDialog::entry_row(&crate::i18n::gettext("Name")); @@ -83,6 +90,7 @@ impl IndexDialog { shown_keys: RefCell::default(), columns, existing, + descending, draft: RefCell::default(), name_edited: Cell::new(false), refresh_queued: Cell::new(false), @@ -208,15 +216,17 @@ impl Inner { if key.descending { row.set_subtitle(&crate::i18n::gettext("Descending")); } - let order = gio::Menu::new(); - order.append(Some(&crate::i18n::gettext("Sort _Descending")), Some("key.descending")); let moves = gio::Menu::new(); moves.append(Some(&crate::i18n::gettext("Move _Up")), Some("key.move-up")); moves.append(Some(&crate::i18n::gettext("Move D_own")), Some("key.move-down")); let removal = gio::Menu::new(); removal.append(Some(&crate::i18n::gettext("_Remove")), Some("key.remove")); let menu = gio::Menu::new(); - menu.append_section(None, &order); + if self.descending { + let order = gio::Menu::new(); + order.append(Some(&crate::i18n::gettext("Sort _Descending")), Some("key.descending")); + menu.append_section(None, &order); + } menu.append_section(None, &moves); menu.append_section(None, &removal); let button = gtk::MenuButton::builder() @@ -229,14 +239,16 @@ impl Inner { row.add_suffix(&button); let actions = gio::SimpleActionGroup::new(); - let descending = gio::SimpleAction::new_stateful("descending", None, &key.descending.to_variant()); - let weak = Rc::downgrade(self); - descending.connect_activate(move |action, _| { - let next = !action.state().and_then(|state| state.get::()).unwrap_or(false); - action.set_state(&next.to_variant()); - edit_later(&weak, move |draft| draft.set_descending(position, next)); - }); - actions.add_action(&descending); + if self.descending { + let descending = gio::SimpleAction::new_stateful("descending", None, &key.descending.to_variant()); + let weak = Rc::downgrade(self); + descending.connect_activate(move |action, _| { + let next = !action.state().and_then(|state| state.get::()).unwrap_or(false); + action.set_state(&next.to_variant()); + edit_later(&weak, move |draft| draft.set_descending(position, next)); + }); + actions.add_action(&descending); + } let steps = [ ("move-up", position > 0, Edit::Up), ("move-down", position + 1 < count, Edit::Down), @@ -296,10 +308,15 @@ mod tests { use crate::test_support::{drain_main_context, wait_until}; fn dialog(added: &Rc>>) -> IndexDialog { + dialog_sorting(added, true) + } + + fn dialog_sorting(added: &Rc>>, descending: bool) -> IndexDialog { let sink = added.clone(); IndexDialog::new( vec!["id".to_owned(), "email".to_owned(), "created_at".to_owned()], vec!["users_pkey".to_owned()], + descending, move |index| sink.borrow_mut().push(index), ) } @@ -424,6 +441,19 @@ mod tests { assert_eq!(first.subtitle().as_deref(), Some("Descending")); } + #[gtk4::test] + fn an_engine_that_keeps_keys_ascending_offers_no_descending_sort() { + let window = window(); + let added = Rc::new(RefCell::new(Vec::new())); + let dialog = dialog_sorting(&added, false); + dialog.present(window.upcast_ref()); + add_key(&dialog, "email"); + + let row = dialog.inner.key_rows.borrow()[0].clone(); + assert!(row.activate_action("key.descending", None).is_err()); + assert!(row.activate_action("key.remove", None).is_ok()); + } + #[gtk4::test] fn removing_the_last_key_leaves_the_form_invalid() { adw::init().expect("libadwaita initialises under the test backend"); diff --git a/linux/crates/app/src/ui/structure_tab/layout.rs b/linux/crates/app/src/ui/structure_tab/layout.rs index 2e40246ea0..f9ac78f451 100644 --- a/linux/crates/app/src/ui/structure_tab/layout.rs +++ b/linux/crates/app/src/ui/structure_tab/layout.rs @@ -185,23 +185,28 @@ fn columns_page(tab: &StructureTab, context: &Rc>, col /// the names its indexes already take. fn indexes_page(tab: &StructureTab, context: &Rc>, stores: &DraftStores<'_>) { let imp = tab.imp(); - let (context, sender) = (context.clone(), tab.sender()); + let (listed, sender) = (context.clone(), tab.sender()); bind_list(&imp.indexes_list, stores.indexes, move |index: &IndexObject| { - let context = context.borrow(); - let removable = context.table_permission(context.capabilities().drop_index); + let context = listed.borrow(); + let removable = context + .table_permission(context.capabilities().drop_index) + .and(|| context.index_removal(index.info())); build_index_row(index, &removable, &sender) }); + let context = Rc::downgrade(context); let (columns, indexes, sender) = (stores.columns.downgrade(), stores.indexes.downgrade(), tab.sender()); imp.add_index_row.connect_activated(move |row| { - let (Some(columns), Some(indexes)) = (columns.upgrade(), indexes.upgrade()) else { + let (Some(columns), Some(indexes), Some(context)) = (columns.upgrade(), indexes.upgrade(), context.upgrade()) + else { return; }; + let descending = context.borrow().capabilities().descending_index_keys; let existing = items::(&indexes) .iter() .map(|index| index.info().name.clone()) .collect(); let sender = sender.clone(); - IndexDialog::new(column_names(&columns), existing, move |index| { + IndexDialog::new(column_names(&columns), existing, descending, move |index| { sender.input(IndexesInput::AddIndex(index)); }) .present(row.upcast_ref()); diff --git a/linux/crates/core/src/browse/build_export.rs b/linux/crates/core/src/browse/build_export.rs index ff1ceca59b..548530ec5e 100644 --- a/linux/crates/core/src/browse/build_export.rs +++ b/linux/crates/core/src/browse/build_export.rs @@ -13,16 +13,17 @@ use super::{BrowseSqlError, ExportRequest}; /// The same projections and filter as a page, so an export holds what /// the grid showed. No engine row id is selected: it is never data the /// user asked for. A key still orders the rows, so two exports of an -/// unchanged table come out the same. A heap address orders them only -/// behind the user's sort: on its own it would sort the whole table -/// before the first row could be written. +/// unchanged table come out the same. A heap address, or a key the +/// engine would sort the whole table by, orders them only behind the +/// user's sort: on its own it would hold every row before the first +/// could be written. pub fn build_export(dialect: &dyn SqlDialect, request: &ExportRequest<'_>) -> Result { let projection = projection_list(dialect, request.columns)?; let components = identity_components(request.identity, request.columns)?; let user = user_order(dialect, request.table, request.columns, request.sort)?; - let heap_order_only = - user.is_empty() && matches!(request.identity, RowIdentity::EngineRowId(id) if id.is_heap_address()); - let order = match heap_order_only { + let sorts_the_table = dialect.key_order_sorts_the_table() + || matches!(request.identity, RowIdentity::EngineRowId(id) if id.is_heap_address()); + let order = match user.is_empty() && sorts_the_table { true => Vec::new(), false => with_tiebreak(user, &identity_order(dialect, request.table, &components)?), }; diff --git a/linux/crates/core/src/browse/tests.rs b/linux/crates/core/src/browse/tests.rs index 44b5bf3604..49569d2470 100644 --- a/linux/crates/core/src/browse/tests.rs +++ b/linux/crates/core/src/browse/tests.rs @@ -524,9 +524,13 @@ fn build_export_projects_only_visible_columns() { } fn export_sql(identity: RowIdentity, sort: &[SortKey]) -> String { + export_sql_in(&TestDialect::exact(), identity, sort) +} + +fn export_sql_in(dialect: &TestDialect, identity: RowIdentity, sort: &[SortKey]) -> String { let columns = columns(); let statement = build_export( - &TestDialect::exact(), + dialect, &ExportRequest { table: &table(), columns: &columns, @@ -559,6 +563,27 @@ fn an_unsorted_export_of_a_heap_is_not_sorted_by_row_address() { ); } +#[test] +fn an_unsorted_export_keeps_the_scan_order_where_the_key_order_is_a_sort() { + let dialect = TestDialect::exact().sorting_by_key(); + + assert_eq!( + export_sql_in(&dialect, by_id(), &[]), + r#"SELECT "id", "name" FROM "public"."users""# + ); + assert_eq!( + export_sql_in( + &dialect, + by_id(), + &[SortKey { + column: 1, + ascending: false + }] + ), + r#"SELECT "id", "name" FROM "public"."users" ORDER BY "users"."name" DESC, "users"."id" ASC"# + ); +} + #[test] fn a_page_splits_into_keys_the_dml_can_bind_back() { let dialect = TestDialect::exact(); diff --git a/linux/crates/core/src/capabilities.rs b/linux/crates/core/src/capabilities.rs index 6a7c6fc125..530f24dece 100644 --- a/linux/crates/core/src/capabilities.rs +++ b/linux/crates/core/src/capabilities.rs @@ -1,4 +1,5 @@ use crate::auth_mode::AuthMode; +use crate::file_format::FileFormat; use crate::meta::ObjectKind; use crate::tls_capabilities::TlsCapabilities; use crate::write_ledger::WriteAtomicity; @@ -7,7 +8,7 @@ use crate::write_ledger::WriteAtomicity; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EndpointKind { Network { default_port: u16 }, - File, + File(FileFormat), } /// How a read-only connection is held to it. @@ -76,12 +77,20 @@ impl DriverCapabilities { pub fn default_port(&self) -> Option { match self.endpoint { EndpointKind::Network { default_port } => Some(default_port), - EndpointKind::File => None, + EndpointKind::File(_) => None, } } pub fn is_file_based(&self) -> bool { - matches!(self.endpoint, EndpointKind::File) + self.file_format().is_some() + } + + /// The engine's database files, for an engine that opens one. + pub fn file_format(&self) -> Option<&FileFormat> { + match &self.endpoint { + EndpointKind::File(format) => Some(format), + EndpointKind::Network { .. } => None, + } } pub fn supports(&self, mode: AuthMode) -> bool { @@ -92,6 +101,7 @@ impl DriverCapabilities { #[cfg(test)] mod tests { use super::*; + use crate::file_format::FileSignature; const NETWORK: DriverCapabilities = DriverCapabilities { endpoint: EndpointKind::Network { default_port: 5432 }, @@ -109,14 +119,24 @@ mod tests { #[test] fn a_file_engine_has_no_port_to_offer() { + let format = FileFormat { + signature: FileSignature { + offset: 0, + bytes: b"SQLite format 3\0", + }, + suffixes: &["sqlite3"], + mime_types: &[], + empty_file_is_database: true, + }; let file = DriverCapabilities { - endpoint: EndpointKind::File, + endpoint: EndpointKind::File(format), auth_modes: &[], ..NETWORK }; assert_eq!(file.default_port(), None); assert!(file.is_file_based()); + assert_eq!(file.file_format(), Some(&format)); assert!(!file.supports(AuthMode::Password), "a file has no login"); } @@ -124,6 +144,7 @@ mod tests { fn a_network_engine_offers_its_port_and_its_logins() { assert_eq!(NETWORK.default_port(), Some(5432)); assert!(!NETWORK.is_file_based()); + assert_eq!(NETWORK.file_format(), None); assert!(NETWORK.supports(AuthMode::Password)); assert!(!NETWORK.supports(AuthMode::Kerberos)); } diff --git a/linux/crates/core/src/ddl/ddl_capabilities.rs b/linux/crates/core/src/ddl/ddl_capabilities.rs index 2cb535f410..995daedc0d 100644 --- a/linux/crates/core/src/ddl/ddl_capabilities.rs +++ b/linux/crates/core/src/ddl/ddl_capabilities.rs @@ -19,6 +19,9 @@ pub struct DdlCapabilities { pub create_index: bool, pub drop_index: bool, pub expression_index_keys: bool, + /// Whether an index keeps a key in descending order. DuckDB takes + /// `DESC` in CREATE INDEX and drops it. + pub descending_index_keys: bool, pub partial_indexes: bool, pub add_foreign_key: bool, pub drop_foreign_key: bool, diff --git a/linux/crates/core/src/ddl/ddl_dialect.rs b/linux/crates/core/src/ddl/ddl_dialect.rs index d64d05aaa5..79d4c74c34 100644 --- a/linux/crates/core/src/ddl/ddl_dialect.rs +++ b/linux/crates/core/src/ddl/ddl_dialect.rs @@ -1,9 +1,9 @@ use super::{ BuildDdlError, ColumnCapabilities, ColumnDefinition, DdlCapabilities, DdlOperation, DdlPlan, DropTableOptions, - ReferentialAction, TableChanges, TableDefinition, TableDraft, + Permission, ReferentialAction, TableChanges, TableDefinition, TableDraft, }; use crate::dialect::SqlDialect; -use crate::meta::TableRef; +use crate::meta::{IndexInfo, TableRef}; /// How one engine plans structure changes. /// @@ -32,6 +32,13 @@ pub trait DdlDialect: Send + Sync + std::fmt::Debug { ColumnCapabilities::standard(self.capabilities(), table, column) } + /// Whether one of the table's indexes can be dropped, where the + /// table's `drop_index` allows it: an index a constraint owns may + /// not be. + fn index_removal(&self, _table: &TableDefinition, _index: &IndexInfo) -> Permission { + Permission::Allowed + } + /// Types offered in the structure tab, most common first. Each one /// parses through `SqlDialect::parse_type`, and any other type can /// still be typed. diff --git a/linux/crates/core/src/ddl/ddl_operation.rs b/linux/crates/core/src/ddl/ddl_operation.rs index 7fa9561b58..264b8d754c 100644 --- a/linux/crates/core/src/ddl/ddl_operation.rs +++ b/linux/crates/core/src/ddl/ddl_operation.rs @@ -22,6 +22,7 @@ pub enum DdlOperation { SetColumnComment, CreateIndex, IndexExpressionKey, + DescendingIndexKey, PartialIndex, DropIndex, AddForeignKey, @@ -48,6 +49,7 @@ impl fmt::Display for DdlOperation { Self::SetColumnComment => "setting a column comment", Self::CreateIndex => "creating an index", Self::IndexExpressionKey => "an index on an expression", + Self::DescendingIndexKey => "a descending index key", Self::PartialIndex => "a partial index", Self::DropIndex => "dropping an index", Self::AddForeignKey => "adding a foreign key", diff --git a/linux/crates/core/src/ddl/table_changes.rs b/linux/crates/core/src/ddl/table_changes.rs index a876498a5f..470978a42c 100644 --- a/linux/crates/core/src/ddl/table_changes.rs +++ b/linux/crates/core/src/ddl/table_changes.rs @@ -92,6 +92,11 @@ impl TableChanges { if !self.dropped_indexes.is_empty() { require(capabilities.drop_index, DdlOperation::DropIndex)?; } + for index in &self.dropped_indexes { + if !ddl.index_removal(original, index).is_allowed() { + return Err(BuildDdlError::Unsupported(DdlOperation::DropIndex)); + } + } for column in &self.dropped_columns { let permission = ddl.column_capabilities(original, column).drop; permit(permission, column.name(), DdlOperation::DropColumn)?; @@ -190,6 +195,9 @@ pub(super) fn check_new_indexes(ddl: &dyn DdlDialect, indexes: &[IndexInfo]) -> if index.keys.iter().any(|key| matches!(key, IndexKey::Expression { .. })) { require(capabilities.expression_index_keys, DdlOperation::IndexExpressionKey)?; } + if index.keys.iter().any(IndexKey::descending) { + require(capabilities.descending_index_keys, DdlOperation::DescendingIndexKey)?; + } if index.predicate.is_some() { require(capabilities.partial_indexes, DdlOperation::PartialIndex)?; } diff --git a/linux/crates/core/src/ddl/tests.rs b/linux/crates/core/src/ddl/tests.rs index 2f9c487c6a..acc5e047a2 100644 --- a/linux/crates/core/src/ddl/tests.rs +++ b/linux/crates/core/src/ddl/tests.rs @@ -422,6 +422,7 @@ fn every_change() -> DdlCapabilities { create_index: true, drop_index: true, expression_index_keys: true, + descending_index_keys: true, partial_indexes: true, add_foreign_key: true, drop_foreign_key: true, diff --git a/linux/crates/core/src/dialect/sql_dialect.rs b/linux/crates/core/src/dialect/sql_dialect.rs index 61d0d6e4b2..b7db24e304 100644 --- a/linux/crates/core/src/dialect/sql_dialect.rs +++ b/linux/crates/core/src/dialect/sql_dialect.rs @@ -112,6 +112,14 @@ pub trait SqlDialect: Send + Sync + std::fmt::Debug { "COUNT(*)" } + /// Whether ordering by the table's key sorts the whole table before + /// the first row comes back, while a plain scan returns an unchanged + /// table's rows in the same order every time. An export with no sort + /// of the user's then keeps the scan's order. + fn key_order_sorts_the_table(&self) -> bool { + false + } + /// The column as text, where it meets a LIKE pattern or text the user /// typed, for a type the engine will not compare with text as it is. /// `None` when the column takes text as it is. diff --git a/linux/crates/core/src/dialect/test_dialect.rs b/linux/crates/core/src/dialect/test_dialect.rs index 348df538cd..c028dee9a7 100644 --- a/linux/crates/core/src/dialect/test_dialect.rs +++ b/linux/crates/core/src/dialect/test_dialect.rs @@ -27,6 +27,7 @@ pub struct TestDialect { row_ids: bool, default_row: bool, count_all: &'static str, + key_order_sorts: bool, grammar: SqlGrammar, insert_limits: InsertBatchLimits, binds: Mutex>, @@ -65,6 +66,7 @@ impl Default for TestDialect { row_ids: true, default_row: true, count_all: "COUNT(*)", + key_order_sorts: false, grammar: SqlGrammar::PostgreSql, insert_limits: InsertBatchLimits::DEFAULT, binds: Mutex::new(Vec::new()), @@ -103,6 +105,13 @@ impl TestDialect { Self { count_all, ..self } } + pub fn sorting_by_key(self) -> Self { + Self { + key_order_sorts: true, + ..self + } + } + pub fn parsing_as(self, grammar: SqlGrammar) -> Self { Self { grammar, ..self } } @@ -248,6 +257,10 @@ impl SqlDialect for TestDialect { self.count_all } + fn key_order_sorts_the_table(&self) -> bool { + self.key_order_sorts + } + fn insert_batch_limits(&self) -> InsertBatchLimits { self.insert_limits } diff --git a/linux/crates/core/src/file_format.rs b/linux/crates/core/src/file_format.rs new file mode 100644 index 0000000000..bd5fcf22b5 --- /dev/null +++ b/linux/crates/core/src/file_format.rs @@ -0,0 +1,99 @@ +use std::path::Path; + +/// Bytes every database file of an engine holds at a fixed place, which +/// is how a file says which engine wrote it whatever its name. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FileSignature { + pub offset: usize, + pub bytes: &'static [u8], +} + +impl FileSignature { + /// Whether `header`, the first bytes of a file, carries the + /// signature. An empty signature matches nothing. + pub fn matches(&self, header: &[u8]) -> bool { + let end = self.offset + self.bytes.len(); + !self.bytes.is_empty() && header.get(self.offset..end) == Some(self.bytes) + } + + /// How much of a file's head has to be read to check it. + pub fn header_len(&self) -> usize { + self.offset + self.bytes.len() + } +} + +/// How a file engine's database files are told apart from any other +/// file: what the file chooser filters on and what decides which engine +/// opens a file handed over from Files or the command line. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FileFormat { + pub signature: FileSignature, + /// Name suffixes without the dot, the one a new file gets first. + pub suffixes: &'static [&'static str], + /// The shared MIME database's types for the engine's files. + pub mime_types: &'static [&'static str], + /// Whether a file of no bytes opens as an empty database, as SQLite + /// reads one. Such a file carries no signature. + pub empty_file_is_database: bool, +} + +impl FileFormat { + /// The suffix a new database file gets, without the dot. + pub fn new_file_suffix(&self) -> Option<&'static str> { + self.suffixes.first().copied() + } + + /// Whether `path` ends in one of the engine's suffixes, in any case. + pub fn names(&self, path: &Path) -> bool { + path.extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| { + self.suffixes + .iter() + .any(|suffix| suffix.eq_ignore_ascii_case(extension)) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const DUCK: FileFormat = FileFormat { + signature: FileSignature { + offset: 8, + bytes: b"DUCK", + }, + suffixes: &["duckdb", "ddb"], + mime_types: &[], + empty_file_is_database: false, + }; + + #[test] + fn a_signature_matches_at_its_offset_only() { + let mut header = vec![0u8; 16]; + header[8..12].copy_from_slice(b"DUCK"); + + assert!(DUCK.signature.matches(&header)); + assert!(!DUCK.signature.matches(&header[..11]), "a short header cannot carry it"); + assert!(!DUCK.signature.matches(b"DUCK000000000000")); + assert_eq!(DUCK.signature.header_len(), 12); + } + + #[test] + fn an_empty_signature_matches_nothing() { + let none = FileSignature { offset: 0, bytes: b"" }; + + assert!(!none.matches(b"")); + assert!(!none.matches(b"anything")); + } + + #[test] + fn a_name_matches_a_suffix_in_any_case() { + assert!(DUCK.names(Path::new("/srv/app.duckdb"))); + assert!(DUCK.names(Path::new("/srv/APP.DDB"))); + assert!(!DUCK.names(Path::new("/srv/app.duckdb.wal"))); + assert!(!DUCK.names(Path::new("/srv/duckdb"))); + assert_eq!(DUCK.new_file_suffix(), Some("duckdb")); + } +} diff --git a/linux/crates/core/src/lib.rs b/linux/crates/core/src/lib.rs index 573a9baa69..2191338d6a 100644 --- a/linux/crates/core/src/lib.rs +++ b/linux/crates/core/src/lib.rs @@ -24,6 +24,7 @@ mod error; mod error_category; pub mod export; mod file_endpoint; +mod file_format; mod file_open_mode; pub mod filter; mod foreign_key_check; @@ -102,6 +103,7 @@ pub use engine_stop::{CallResource, EngineStop, StopDelivery, StopScope}; pub use error::DriverError; pub use error_category::ErrorCategory; pub use file_endpoint::FileEndpoint; +pub use file_format::{FileFormat, FileSignature}; pub use file_open_mode::FileOpenMode; pub use filter::{ BuildFilterError, Combinator, FilterOp, FilterRule, FilterSet, FilterValue, build_filter, operators_for, diff --git a/linux/crates/core/src/registry.rs b/linux/crates/core/src/registry.rs index 81159b8af2..aca2c139a9 100644 --- a/linux/crates/core/src/registry.rs +++ b/linux/crates/core/src/registry.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use std::sync::Arc; use crate::driver::DatabaseDriver; @@ -32,4 +33,211 @@ impl DriverRegistry { pub fn is_empty(&self) -> bool { self.drivers.is_empty() } + + /// How many bytes of a file `file_driver_for` needs to see. + pub fn file_header_len(&self) -> usize { + self.file_drivers() + .filter_map(|driver| driver.capabilities().file_format()) + .map(|format| format.signature.header_len()) + .max() + .unwrap_or(0) + } + + /// The engine that opens the file at `path`, whose first bytes are + /// `header`. + /// + /// What the file holds decides first, since a name can be anything. + /// A file too short to carry any signature goes by its name, so the + /// engine it names reports what is wrong with it, and an empty file + /// with a name no engine claims goes to the one engine that reads an + /// empty file as a new database. Anything else is no engine's. + pub fn file_driver_for(&self, header: &[u8], path: &Path) -> Option> { + let format_of = |driver: &&Arc| driver.capabilities().file_format().copied(); + if let Some(signed) = self + .file_drivers() + .find(|driver| format_of(driver).is_some_and(|format| format.signature.matches(header))) + { + return Some(signed.clone()); + } + let shortest = self + .file_drivers() + .filter_map(|driver| format_of(&driver)) + .map(|format| format.signature.header_len()) + .min() + .unwrap_or(0); + if header.len() >= shortest { + return None; + } + if let Some(named) = self + .file_drivers() + .find(|driver| format_of(driver).is_some_and(|format| format.names(path))) + { + return Some(named.clone()); + } + if !header.is_empty() { + return None; + } + let mut empty_readers = self + .file_drivers() + .filter(|driver| format_of(driver).is_some_and(|format| format.empty_file_is_database)); + match (empty_readers.next(), empty_readers.next()) { + (Some(only), None) => Some(only.clone()), + _ => None, + } + } + + fn file_drivers(&self) -> impl Iterator> { + self.drivers + .values() + .filter(|driver| driver.capabilities().is_file_based()) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use async_trait::async_trait; + use tokio_util::sync::CancellationToken; + + use super::*; + use crate::capabilities::{ + CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, ReadOnlyEnforcement, + ScriptExecution, + }; + use crate::connect_options::ConnectOptions; + use crate::connection::Connection; + use crate::error::DriverError; + use crate::file_format::{FileFormat, FileSignature}; + use crate::write_ledger::WriteAtomicity; + + const fn capabilities(endpoint: EndpointKind) -> DriverCapabilities { + DriverCapabilities { + endpoint, + auth_modes: &[], + tls: None, + read_only: ReadOnlyEnforcement::EngineEnforced, + dml_writes: WriteAtomicity::Transactional, + ddl_writes: WriteAtomicity::Transactional, + script_execution: ScriptExecution::NativeBatch, + commit_resolution: CommitResolutionSupport::Unavailable, + row_count_estimate: EstimateQuality::Unavailable, + object_kinds: &[], + database_required: false, + } + } + + static SQLITE: DriverCapabilities = capabilities(EndpointKind::File(FileFormat { + signature: FileSignature { + offset: 0, + bytes: b"SQLite format 3\0", + }, + suffixes: &["sqlite3", "sqlite", "db", "db3"], + mime_types: &["application/vnd.sqlite3"], + empty_file_is_database: true, + })); + + static DUCKDB: DriverCapabilities = capabilities(EndpointKind::File(FileFormat { + signature: FileSignature { + offset: 8, + bytes: b"DUCK", + }, + suffixes: &["duckdb", "ddb"], + mime_types: &[], + empty_file_is_database: false, + })); + + static NETWORK: DriverCapabilities = capabilities(EndpointKind::Network { default_port: 5432 }); + + struct Engine(&'static str, &'static DriverCapabilities); + + #[async_trait] + impl DatabaseDriver for Engine { + fn id(&self) -> &'static str { + self.0 + } + + fn display_name(&self) -> &'static str { + self.0 + } + + fn capabilities(&self) -> &'static DriverCapabilities { + self.1 + } + + async fn connect( + &self, + _options: ConnectOptions, + _cancel: CancellationToken, + ) -> Result, DriverError> { + Err(DriverError::Cancelled) + } + } + + fn registry() -> DriverRegistry { + let mut registry = DriverRegistry::new(); + registry.register(Arc::new(Engine("postgres", &NETWORK))); + registry.register(Arc::new(Engine("sqlite", &SQLITE))); + registry.register(Arc::new(Engine("duckdb", &DUCKDB))); + registry + } + + fn chosen(registry: &DriverRegistry, header: &[u8], name: &str) -> Option<&'static str> { + registry + .file_driver_for(header, &PathBuf::from("/srv").join(name)) + .map(|driver| driver.id()) + } + + fn duckdb_header() -> Vec { + let mut header = vec![0x8b, 0x88, 0x5d, 0xf0, 0xba, 0x46, 0x13, 0x98]; + header.extend_from_slice(b"DUCK"); + header.extend_from_slice(&[0; 4]); + header + } + + #[test] + fn what_a_file_holds_decides_over_its_name() { + let registry = registry(); + + assert_eq!(chosen(®istry, &duckdb_header(), "data.db"), Some("duckdb")); + assert_eq!(chosen(®istry, &duckdb_header(), "data.sqlite3"), Some("duckdb")); + assert_eq!( + chosen(®istry, b"SQLite format 3\0rest", "data.duckdb"), + Some("sqlite") + ); + assert_eq!(chosen(®istry, b"SQLite format 3\0", "notes"), Some("sqlite")); + } + + #[test] + fn a_file_with_no_signature_goes_by_its_name() { + let registry = registry(); + + assert_eq!(chosen(®istry, b"", "new.duckdb"), Some("duckdb")); + assert_eq!(chosen(®istry, b"", "new.sqlite3"), Some("sqlite")); + assert_eq!(chosen(®istry, b"garbage", "broken.ddb"), Some("duckdb")); + assert_eq!(chosen(®istry, b"garbage", "notes.txt"), None); + assert_eq!( + chosen(®istry, b"plain text, long enough", "notes.db"), + None, + "a header long enough to carry a signature carries none" + ); + } + + #[test] + fn an_empty_unclaimed_file_opens_as_the_engine_that_reads_one() { + let registry = registry(); + + assert_eq!(chosen(®istry, b"", "untitled"), Some("sqlite")); + assert_eq!( + chosen(&DriverRegistry::new(), b"", "untitled"), + None, + "no engine opens files" + ); + } + + #[test] + fn the_header_read_covers_every_signature() { + assert_eq!(registry().file_header_len(), 16); + assert_eq!(DriverRegistry::new().file_header_len(), 0); + } } diff --git a/linux/crates/core/src/server_diagnostics.rs b/linux/crates/core/src/server_diagnostics.rs index d7190cff5d..c622939d12 100644 --- a/linux/crates/core/src/server_diagnostics.rs +++ b/linux/crates/core/src/server_diagnostics.rs @@ -9,10 +9,27 @@ use std::fmt; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ServerCode { SqlState(String), - MySql { number: u16, sqlstate: Option }, - Tds { number: u32, state: u8, class: u8 }, - ClickHouse { code: i32 }, - Sqlite { primary: i32, extended: i32 }, + MySql { + number: u16, + sqlstate: Option, + }, + Tds { + number: u32, + state: u8, + class: u8, + }, + ClickHouse { + code: i32, + }, + Sqlite { + primary: i32, + extended: i32, + }, + /// DuckDB names its errors by kind rather than number, as the + /// `Constraint` of "Constraint Error: ...". + DuckDb { + error_type: String, + }, } impl ServerCode { @@ -34,6 +51,7 @@ impl fmt::Display for ServerCode { Self::Tds { number, .. } => write!(f, "{number}"), Self::ClickHouse { code } => write!(f, "{code}"), Self::Sqlite { extended, .. } => write!(f, "{extended}"), + Self::DuckDb { error_type } => f.write_str(error_type), } } } diff --git a/linux/crates/core/src/sql_syntax/grammar.rs b/linux/crates/core/src/sql_syntax/grammar.rs index 1e29982291..0dd1b5ae5c 100644 --- a/linux/crates/core/src/sql_syntax/grammar.rs +++ b/linux/crates/core/src/sql_syntax/grammar.rs @@ -5,15 +5,17 @@ pub enum SqlGrammar { Sqlite, MsSql, ClickHouse, + DuckDb, } impl SqlGrammar { - pub const ALL: [SqlGrammar; 5] = [ + pub const ALL: [SqlGrammar; 6] = [ SqlGrammar::PostgreSql, SqlGrammar::MySql, SqlGrammar::Sqlite, SqlGrammar::MsSql, SqlGrammar::ClickHouse, + SqlGrammar::DuckDb, ]; } @@ -22,13 +24,16 @@ impl SqlGrammar { /// private: no public signature names a sqlparser type, so the /// parser stays replaceable. pub(crate) fn parser_dialect(self) -> &'static dyn sqlparser::dialect::Dialect { - use sqlparser::dialect::{ClickHouseDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect}; + use sqlparser::dialect::{ + ClickHouseDialect, DuckDbDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, + }; static POSTGRES: PostgreSqlDialect = PostgreSqlDialect {}; static MYSQL: MySqlDialect = MySqlDialect {}; static SQLITE: SQLiteDialect = SQLiteDialect {}; static MSSQL: MsSqlDialect = MsSqlDialect {}; static CLICKHOUSE: ClickHouseDialect = ClickHouseDialect {}; + static DUCKDB: DuckDbDialect = DuckDbDialect {}; match self { Self::PostgreSql => &POSTGRES, @@ -36,6 +41,7 @@ impl SqlGrammar { Self::Sqlite => &SQLITE, Self::MsSql => &MSSQL, Self::ClickHouse => &CLICKHOUSE, + Self::DuckDb => &DUCKDB, } } } diff --git a/linux/crates/core/src/sql_syntax/keywords.rs b/linux/crates/core/src/sql_syntax/keywords.rs index 4f3717f6ad..79e0e1ce7c 100644 --- a/linux/crates/core/src/sql_syntax/keywords.rs +++ b/linux/crates/core/src/sql_syntax/keywords.rs @@ -138,6 +138,33 @@ const CLICKHOUSE: &[&str] = &[ "SAMPLE", "SETTINGS", "SHOW", ]; +const DUCKDB: &[&str] = &[ + "ANTI", + "ASOF", + "ATTACH", + "CHECKPOINT", + "COPY", + "DESCRIBE", + "DETACH", + "EXCLUDE", + "EXPLAIN", + "ILIKE", + "LIMIT", + "OFFSET", + "PIVOT", + "POSITIONAL", + "PRAGMA", + "QUALIFY", + "REPLACE", + "RETURNING", + "SAMPLE", + "SEMI", + "SHOW", + "SUMMARIZE", + "UNPIVOT", + "USE", +]; + impl SqlGrammar { /// The keywords the editor completes for this grammar, in upper case. pub fn keywords(self) -> impl Iterator { @@ -147,6 +174,7 @@ impl SqlGrammar { Self::Sqlite => SQLITE, Self::MsSql => MSSQL, Self::ClickHouse => CLICKHOUSE, + Self::DuckDb => DUCKDB, }; COMMON.iter().chain(own).copied() } @@ -169,6 +197,7 @@ mod tests { assert!(has(SqlGrammar::MsSql, "TOP")); assert!(!has(SqlGrammar::MsSql, "LIMIT")); assert!(has(SqlGrammar::ClickHouse, "PREWHERE")); + assert!(has(SqlGrammar::DuckDb, "QUALIFY")); } #[test] diff --git a/linux/crates/core/src/sql_syntax/reserved_words.rs b/linux/crates/core/src/sql_syntax/reserved_words.rs index 558982f9f5..6159757ab8 100644 --- a/linux/crates/core/src/sql_syntax/reserved_words.rs +++ b/linux/crates/core/src/sql_syntax/reserved_words.rs @@ -4,7 +4,8 @@ //! words of MySQL 8.4 and MariaDB, the T-SQL reserved keywords and all //! of SQLite's keywords. ClickHouse reserves nothing, so its list holds //! the words its parser takes as a clause, an operator or a literal -//! wherever they stand. +//! wherever they stand. DuckDB's are the `reserved` and `type_function` +//! words of its `duckdb_keywords()`. use crate::sql_syntax::SqlGrammar; @@ -84,6 +85,17 @@ const CLICKHOUSE: &str = " SETTINGS THEN TO TOP TRUE UNION USING WHEN WHERE WINDOW WITH "; +const DUCKDB: &str = " + ALL ANALYSE ANALYZE AND ANTI ANY ARRAY AS ASC ASOF ASYMMETRIC AT AUTHORIZATION BINARY BOTH BY CASE CAST + CHECK COLLATE COLLATION COLUMN COLUMNS CONCURRENTLY CONSTRAINT CREATE CROSS DEFAULT DEFERRABLE DESC + DESCRIBE DISTINCT DO ELSE END EXCEPT FALSE FETCH FOR FOREIGN FREEZE FROM FULL GENERATED GLOB GROUP + HAVING ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL JOIN LAMBDA LATERAL LEADING LEFT LIKE LIMIT MAP + NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PIVOT PIVOT_LONGER PIVOT_WIDER PLACING + POSITIONAL PRIMARY QUALIFY REFERENCES RETURNING RIGHT SELECT SEMI SHOW SIMILAR SOME STRUCT SUMMARIZE + SYMMETRIC TABLE TABLESAMPLE THEN TO TRAILING TRUE TRY_CAST UNION UNIQUE UNPACK UNPIVOT USING VARIADIC + VERBOSE WHEN WHERE WINDOW WITH +"; + impl SqlGrammar { /// Whether `name` reads as the same identifier written without /// quotes. @@ -124,6 +136,7 @@ impl SqlGrammar { Self::Sqlite => SQLITE, Self::MsSql => MSSQL, Self::ClickHouse => CLICKHOUSE, + Self::DuckDb => DUCKDB, } } } @@ -155,6 +168,10 @@ mod tests { assert!(SqlGrammar::PostgreSql.is_bare_identifier("đơn_hàng")); assert!(SqlGrammar::MySql.is_bare_identifier("Users")); assert!(SqlGrammar::MsSql.is_bare_identifier("#scratch")); + assert!( + SqlGrammar::DuckDb.is_bare_identifier("Users"), + "DuckDB matches a name in any case" + ); } #[test] @@ -193,5 +210,7 @@ mod tests { } assert!(!SqlGrammar::Sqlite.is_bare_identifier("current_timestamp")); assert!(!SqlGrammar::ClickHouse.is_bare_identifier("nan")); + assert!(!SqlGrammar::DuckDb.is_bare_identifier("qualify")); + assert!(!SqlGrammar::DuckDb.is_bare_identifier("map")); } } diff --git a/linux/crates/core/src/sql_syntax/script/lexical_settings.rs b/linux/crates/core/src/sql_syntax/script/lexical_settings.rs index 841678549e..c62ff69666 100644 --- a/linux/crates/core/src/sql_syntax/script/lexical_settings.rs +++ b/linux/crates/core/src/sql_syntax/script/lexical_settings.rs @@ -9,7 +9,7 @@ impl LexicalSettings { pub fn default_for(grammar: SqlGrammar) -> Self { let backslash_escapes = match grammar { SqlGrammar::MySql | SqlGrammar::ClickHouse => true, - SqlGrammar::PostgreSql | SqlGrammar::Sqlite | SqlGrammar::MsSql => false, + SqlGrammar::PostgreSql | SqlGrammar::Sqlite | SqlGrammar::MsSql | SqlGrammar::DuckDb => false, }; Self { backslash_escapes } } diff --git a/linux/crates/core/src/sql_syntax/script/script_rules.rs b/linux/crates/core/src/sql_syntax/script/script_rules.rs index 7eae866b20..8b56fca67f 100644 --- a/linux/crates/core/src/sql_syntax/script/script_rules.rs +++ b/linux/crates/core/src/sql_syntax/script/script_rules.rs @@ -205,6 +205,32 @@ const CLICKHOUSE: ScriptRules = ScriptRules { }, }; +/// DuckDB's parser is PostgreSQL's scanner without its procedural +/// bodies: a string escapes only with an `E` prefix, block comments +/// nest, `$tag$` quotes, and a backtick quotes nothing. +const DUCKDB: ScriptRules = ScriptRules { + boundary: Boundary::Semicolon, + compound: CompoundRule::None, + parens_hold_semicolons: false, + batch_error: BatchErrorPolicy::StopScript, + syntax: ScriptSyntax { + single_quote: quote(QuoteKind::StringLiteral, BackslashPolicy::Never), + double_quote: quote(QuoteKind::QuotedIdentifier, BackslashPolicy::Never), + backtick: None, + bracket_identifier: BracketIdentifier::None, + escape_string_prefix: true, + dash_comment_needs_space: false, + line_comment_ends_at_cr: true, + hash_comment: HashComment::None, + slash_slash_comment: false, + nested_block_comments: true, + executable_comments: false, + dollar_quote: DollarQuote::Tagged, + word_start_extra: b"", + word_part_extra: b"$", + }, +}; + pub(crate) fn rules_for(grammar: SqlGrammar) -> &'static ScriptRules { match grammar { SqlGrammar::PostgreSql => &POSTGRESQL, @@ -212,5 +238,6 @@ pub(crate) fn rules_for(grammar: SqlGrammar) -> &'static ScriptRules { SqlGrammar::Sqlite => &SQLITE, SqlGrammar::MsSql => &MSSQL, SqlGrammar::ClickHouse => &CLICKHOUSE, + SqlGrammar::DuckDb => &DUCKDB, } } diff --git a/linux/crates/core/tests/script_corpus.rs b/linux/crates/core/tests/script_corpus.rs index d9e7474cf2..7af0c6e7c2 100644 --- a/linux/crates/core/tests/script_corpus.rs +++ b/linux/crates/core/tests/script_corpus.rs @@ -1,7 +1,9 @@ use std::num::NonZeroU32; use proptest::prelude::*; -use sqlparser::dialect::{ClickHouseDialect, Dialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect}; +use sqlparser::dialect::{ + ClickHouseDialect, Dialect, DuckDbDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, +}; use sqlparser::tokenizer::{Token, Tokenizer, TokenizerError}; use tablepro_core::sql_syntax::SqlGrammar; use tablepro_core::sql_syntax::script::{ @@ -387,6 +389,30 @@ fn backslash_always_escapes() { ); } +#[test] +fn duckdb_escapes_only_in_an_e_string() { + let text = r"SELECT E'it\'s; x'; SELECT 'a\'; SELECT 2;"; + assert_eq!( + statements(SqlGrammar::DuckDb, text), + [r"SELECT E'it\'s; x'", r"SELECT 'a\'", "SELECT 2"] + ); +} + +#[test] +fn duckdb_nests_comments_and_quotes_with_dollar_tags() { + let text = "SELECT /* a /* b; */ c; */ $x$it's; here$x$, $$;$$; SELECT 2;"; + assert_eq!( + statements(SqlGrammar::DuckDb, text), + ["SELECT /* a /* b; */ c; */ $x$it's; here$x$, $$;$$", "SELECT 2"] + ); +} + +#[test] +fn duckdb_reads_a_backtick_as_no_quote() { + let text = "SELECT `a;b` FROM t;"; + assert_eq!(statements(SqlGrammar::DuckDb, text), ["SELECT `a", "b` FROM t"]); +} + #[test] fn statement_at_on_terminator_takes_preceding() { let text = "SELECT 1;SELECT 2;"; @@ -505,6 +531,7 @@ const AGREED_CASES: &[(SqlGrammar, &str)] = &[ ), (SqlGrammar::ClickHouse, "SELECT $doc$a; b$doc$; SELECT 2;"), (SqlGrammar::ClickHouse, r"SELECT 'a\'; b', `c;d` FROM t; SELECT 2;"), + (SqlGrammar::DuckDb, "SELECT 'a;b', \"c;d\" FROM t; -- x;\nSELECT 2;"), ]; fn byte_offset(text: &str, line: u64, column: u64) -> usize { @@ -530,6 +557,7 @@ fn sqlparser_semicolons(grammar: SqlGrammar, text: &str) -> Result, T SqlGrammar::Sqlite => &SQLiteDialect {}, SqlGrammar::MsSql => &MsSqlDialect {}, SqlGrammar::ClickHouse => &ClickHouseDialect {}, + SqlGrammar::DuckDb => &DuckDbDialect {}, }; let tokens = Tokenizer::new(dialect, text) .with_unescape(false) diff --git a/linux/crates/drivers/clickhouse/src/ddl.rs b/linux/crates/drivers/clickhouse/src/ddl.rs index 9e1ef7c5a4..a372b9bc68 100644 --- a/linux/crates/drivers/clickhouse/src/ddl.rs +++ b/linux/crates/drivers/clickhouse/src/ddl.rs @@ -30,6 +30,7 @@ static CAPABILITIES: DdlCapabilities = DdlCapabilities { create_index: false, drop_index: true, expression_index_keys: true, + descending_index_keys: false, partial_indexes: false, add_foreign_key: false, drop_foreign_key: false, diff --git a/linux/crates/drivers/duckdb/Cargo.toml b/linux/crates/drivers/duckdb/Cargo.toml new file mode 100644 index 0000000000..1f6b167968 --- /dev/null +++ b/linux/crates/drivers/duckdb/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tablepro-driver-duckdb" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +tablepro-core.workspace = true +async-trait.workspace = true +chrono.workspace = true +deadpool.workspace = true +duckdb.workspace = true +rustix = { workspace = true, features = ["fs"] } +tokio = { workspace = true, features = ["rt", "sync"] } +tokio-util.workspace = true +tracing.workspace = true +uuid.workspace = true + +[dev-dependencies] +tablepro-test-support.workspace = true +tempfile.workspace = true + +[lints] +workspace = true diff --git a/linux/crates/drivers/duckdb/src/capabilities.rs b/linux/crates/drivers/duckdb/src/capabilities.rs new file mode 100644 index 0000000000..7240051116 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/capabilities.rs @@ -0,0 +1,35 @@ +use tablepro_core::meta::ObjectKind; +use tablepro_core::{ + CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, FileFormat, FileSignature, + ReadOnlyEnforcement, ScriptExecution, WriteAtomicity, +}; + +/// Every database file carries `DUCK` eight bytes in, after a checksum. +/// The shared MIME database has no type for it, and an empty file is not +/// a database to DuckDB. +pub(crate) const FILE_FORMAT: FileFormat = FileFormat { + signature: FileSignature { + offset: 8, + bytes: b"DUCK", + }, + suffixes: &["duckdb", "ddb"], + mime_types: &[], + empty_file_is_database: false, +}; + +/// A file with no login and no TLS, opened read-only by DuckDB itself +/// when the connection is. Saves and schema changes are one transaction +/// each, and a COMMIT never crosses a network. +pub(crate) static CAPABILITIES: DriverCapabilities = DriverCapabilities { + endpoint: EndpointKind::File(FILE_FORMAT), + auth_modes: &[], + tls: None, + read_only: ReadOnlyEnforcement::EngineEnforced, + dml_writes: WriteAtomicity::Transactional, + ddl_writes: WriteAtomicity::Transactional, + script_execution: ScriptExecution::NativeBatch, + commit_resolution: CommitResolutionSupport::Unavailable, + row_count_estimate: EstimateQuality::Statistics, + object_kinds: &[ObjectKind::Table, ObjectKind::View], + database_required: false, +}; diff --git a/linux/crates/drivers/duckdb/src/catalog/generated.rs b/linux/crates/drivers/duckdb/src/catalog/generated.rs new file mode 100644 index 0000000000..5569699a2e --- /dev/null +++ b/linux/crates/drivers/duckdb/src/catalog/generated.rs @@ -0,0 +1,101 @@ +//! Which columns of a table are generated. +//! +//! DuckDB's catalogue functions list a generated column's expression as +//! its default and say nothing else about it, so the only record is the +//! CREATE TABLE text DuckDB keeps, where each one reads +//! `name type GENERATED ALWAYS AS (expression)`. + +use tablepro_core::sql_syntax::SqlGrammar; +use tablepro_core::sql_syntax::script::{LexicalSettings, ScriptPlan, ScriptTokenKind}; + +/// The names of the generated columns in `create_sql`, as the table's +/// own text spells them. +pub(crate) fn generated_columns(create_sql: &str) -> Vec { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(create_sql, grammar, LexicalSettings::default_for(grammar)); + let tokens: Vec<_> = plan + .tokens(0..create_sql.len()) + .filter(|token| !token.kind.is_trivia()) + .collect(); + let mut generated = Vec::new(); + let mut depth = 0usize; + let mut element: Vec<(ScriptTokenKind, &str)> = Vec::new(); + for token in &tokens { + let text = create_sql.get(token.range.clone()).unwrap_or_default(); + match token.kind { + ScriptTokenKind::OpenParen => { + depth += 1; + if depth == 1 { + continue; + } + } + ScriptTokenKind::CloseParen => { + depth = depth.saturating_sub(1); + if depth == 0 { + take(&mut element, &mut generated); + continue; + } + } + ScriptTokenKind::Punct if text == "," && depth == 1 => { + take(&mut element, &mut generated); + continue; + } + _ => {} + } + if depth >= 1 { + element.push((token.kind, text)); + } + } + generated +} + +/// Close one element of the column list, keeping its name when it is a +/// generated column. +fn take(element: &mut Vec<(ScriptTokenKind, &str)>, generated: &mut Vec) { + let words: Vec<&str> = element + .iter() + .filter(|(kind, _)| *kind == ScriptTokenKind::Word) + .map(|(_, text)| *text) + .collect(); + let is_generated = words.windows(3).any(|window| { + window + .iter() + .map(|word| word.to_ascii_uppercase()) + .eq(["GENERATED", "ALWAYS", "AS"]) + }); + if is_generated && let Some((kind, name)) = element.first() { + match kind { + ScriptTokenKind::QuotedIdentifier => generated.push(unquote(name)), + ScriptTokenKind::Word => generated.push((*name).to_owned()), + _ => {} + } + } + element.clear(); +} + +fn unquote(quoted: &str) -> String { + quoted + .strip_prefix('"') + .and_then(|inner| inner.strip_suffix('"')) + .unwrap_or(quoted) + .replace("\"\"", "\"") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_columns_are_found_in_the_stored_create_statement() { + let sql = "CREATE TABLE child(id BIGINT DEFAULT(nextval('s1')), pid INTEGER, \ + total INTEGER GENERATED ALWAYS AS((pid * 2)), \"odd, name\" VARCHAR GENERATED ALWAYS AS((CAST(pid AS VARCHAR))), \ + note VARCHAR DEFAULT('generated always as'), FOREIGN KEY (pid) REFERENCES parent(id), PRIMARY KEY(id));"; + + assert_eq!(generated_columns(sql), ["total", "odd, name"]); + } + + #[test] + fn a_table_with_none_has_none() { + assert!(generated_columns("CREATE TABLE t(a INTEGER, b VARCHAR);").is_empty()); + } +} diff --git a/linux/crates/drivers/duckdb/src/catalog/mod.rs b/linux/crates/drivers/duckdb/src/catalog/mod.rs new file mode 100644 index 0000000000..5040514e78 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/catalog/mod.rs @@ -0,0 +1,601 @@ +//! What the database holds, read from DuckDB's `duckdb_*()` functions. +//! +//! Every read names the file's own catalogue: a database attached in an +//! editor tab lists in the same functions, and its tables are not this +//! connection's to browse. The functions work with no extension loaded. + +mod generated; + +use std::collections::HashSet; + +use tablepro_core::column::{ColumnDefault, ColumnInfo, ColumnType, SqlExpression}; +use tablepro_core::ddl::{ + ColumnAttributes, ColumnDefinition, GeneratedColumn, GeneratedStorage, IdentityStyle, InboundForeignKey, + PrimaryKeyConstraint, ReferentialAction, SchemaObjectKind, SchemaObjectSource, TableDefinition, TableSource, +}; +use tablepro_core::edit::parse_literal_text; +use tablepro_core::meta::{ + IndexInfo, IndexKey, ObjectInfo, ObjectKind, ObjectScope, RowCountEstimate, RowIdentity, SchemaInfo, TableEstimate, + TableRef, +}; +use tablepro_core::sql_syntax::{LiteralToken, MAX_EXPRESSION_LEN, SqlGrammar, parse_expression}; +use tablepro_core::value::Value; +use tablepro_core::{DriverError, ForeignKeyInfo}; + +use self::generated::generated_columns; +use crate::duck_type::column_type_from_text; +use crate::errors::map_error; +use crate::instance::Link; + +/// The schema a name without one lives in. +pub(crate) const DEFAULT_SCHEMA: &str = "main"; + +fn schema_of(table: &TableRef) -> &str { + table + .schema + .as_deref() + .filter(|schema| !schema.is_empty()) + .unwrap_or(DEFAULT_SCHEMA) +} + +/// Rows of a catalogue query whose columns are all text or NULL. +fn text_rows(link: &Link, sql: &str, params: &[&str]) -> Result>>, DriverError> { + let mut statement = link.connection.prepare(sql).map_err(map_error)?; + let mut rows = statement + .query(duckdb::params_from_iter(params.iter())) + .map_err(map_error)?; + let width = rows.as_ref().map_or(0, duckdb::Statement::column_count); + let mut read = Vec::new(); + while let Some(row) = rows.next().map_err(map_error)? { + let mut values = Vec::with_capacity(width); + for index in 0..width { + values.push(row.get::<_, Option>(index).map_err(map_error)?); + } + read.push(values); + } + Ok(read) +} + +fn text(row: &[Option], index: usize) -> String { + row.get(index).cloned().flatten().unwrap_or_default() +} + +fn flag(row: &[Option], index: usize) -> bool { + row.get(index) + .and_then(Option::as_deref) + .is_some_and(|value| value.eq_ignore_ascii_case("true")) +} + +pub(crate) fn list_schemas(link: &Link, catalog: &str) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT schema_name FROM duckdb_schemas() WHERE database_name = ? ORDER BY schema_name", + &[catalog], + )?; + Ok(rows + .iter() + .map(|row| { + let name = text(row, 0); + SchemaInfo { + is_default: name == DEFAULT_SCHEMA, + is_system: false, + name, + } + }) + .collect()) +} + +pub(crate) fn list_objects(link: &Link, catalog: &str, scope: &ObjectScope) -> Result, DriverError> { + let schema = match scope { + ObjectScope::DefaultSchema => Some(DEFAULT_SCHEMA), + ObjectScope::Schema(schema) => Some(schema.as_str()), + ObjectScope::AllSchemas => None, + }; + let sql = "SELECT schema_name, table_name, 'table' FROM duckdb_tables() \ + WHERE database_name = ? AND NOT temporary AND (? IS NULL OR schema_name = ?) \ + UNION ALL \ + SELECT schema_name, view_name, 'view' FROM duckdb_views() \ + WHERE database_name = ? AND NOT internal AND NOT temporary AND (? IS NULL OR schema_name = ?) \ + ORDER BY 1, 2"; + let mut statement = link.connection.prepare(sql).map_err(map_error)?; + let mut rows = statement + .query(duckdb::params![catalog, schema, schema, catalog, schema, schema]) + .map_err(map_error)?; + let mut objects = Vec::new(); + while let Some(row) = rows.next().map_err(map_error)? { + let kind: String = row.get(2).map_err(map_error)?; + objects.push(ObjectInfo { + schema: Some(row.get(0).map_err(map_error)?), + name: row.get(1).map_err(map_error)?, + kind: match kind.as_str() { + "view" => ObjectKind::View, + _ => ObjectKind::Table, + }, + }); + } + Ok(objects) +} + +/// A table's columns, with the primary key marked and the generated +/// columns told apart from defaults. +pub(crate) fn fetch_columns(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + let schema = schema_of(table); + let rows = text_rows( + link, + "SELECT column_name, data_type, CAST(is_nullable AS VARCHAR), column_default, comment \ + FROM duckdb_columns() WHERE database_name = ? AND schema_name = ? AND table_name = ? \ + ORDER BY column_index", + &[catalog, schema, &table.name], + )?; + let keys = primary_key(link, catalog, table)? + .map(|key| key.columns) + .unwrap_or_default(); + let generated: HashSet = table_sql(link, catalog, table)? + .map(|sql| generated_columns(&sql).into_iter().collect()) + .unwrap_or_default(); + Ok(rows + .iter() + .map(|row| { + let name = text(row, 0); + let column_type = column_type_from_text(&text(row, 1)); + let default_text = row.get(3).cloned().flatten(); + let is_generated = generated.contains(&name); + let default = match is_generated { + true => ColumnDefault::None, + false => classify_default(default_text.as_deref(), &column_type), + }; + ColumnInfo { + primary_key: keys.contains(&name), + nullable: flag(row, 2), + is_auto_increment: !is_generated && is_sequence_default(default_text.as_deref()), + is_generated, + default, + comment: row.get(4).cloned().flatten(), + column_type, + name, + } + }) + .collect()) +} + +fn is_sequence_default(text: Option<&str>) -> bool { + text.is_some_and(|text| text.trim_start().to_ascii_lowercase().starts_with("nextval(")) +} + +/// A DEFAULT as a value the insert path can bind where it is a literal, +/// and as DuckDB's expression otherwise. +pub(crate) fn classify_default(text: Option<&str>, column_type: &ColumnType) -> ColumnDefault { + let Some(text) = text.map(str::trim).filter(|text| !text.is_empty()) else { + return ColumnDefault::None; + }; + let expression = || ColumnDefault::Expression(SqlExpression::from_catalog_text(text)); + let Ok(parsed) = parse_expression(SqlGrammar::DuckDb, text, MAX_EXPRESSION_LEN) else { + return expression(); + }; + let literal_text = match parsed.literal { + None => return expression(), + Some(LiteralToken::Null) => return ColumnDefault::None, + Some(LiteralToken::Text(text)) => text, + Some(LiteralToken::Number(number)) => number.to_string(), + Some(LiteralToken::Boolean(flag)) => flag.to_string(), + }; + match parse_literal_text(&literal_text, column_type) { + Ok(Value::Null) | Err(_) => expression(), + Ok(value) => ColumnDefault::Literal(value), + } +} + +/// The CREATE TABLE statement DuckDB keeps for a table, which a view has +/// none of. +fn table_sql(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT sql FROM duckdb_tables() WHERE database_name = ? AND schema_name = ? AND table_name = ?", + &[catalog, schema_of(table), &table.name], + )?; + Ok(rows.first().and_then(|row| row.first().cloned().flatten())) +} + +/// The columns of a table's PRIMARY KEY and UNIQUE constraints, in key +/// order, by constraint. +fn key_constraints( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result)>, DriverError> { + let rows = text_rows( + link, + "SELECT constraint_name, constraint_type, array_to_string(constraint_column_names, chr(31)) \ + FROM duckdb_constraints() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ? \ + AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') \ + ORDER BY constraint_index", + &[catalog, schema_of(table), &table.name], + )?; + Ok(rows + .iter() + .map(|row| (text(row, 0), text(row, 1), split_names(&text(row, 2)))) + .collect()) +} + +pub(crate) fn primary_key( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result, DriverError> { + Ok(key_constraints(link, catalog, table)? + .into_iter() + .find(|(_, kind, _)| kind == "PRIMARY KEY") + .map(|(name, _, columns)| PrimaryKeyConstraint { + name: Some(name), + columns, + })) +} + +/// The table's indexes: the ones created with CREATE INDEX, and the +/// index behind each PRIMARY KEY and UNIQUE constraint, which DuckDB +/// lists only as the constraint. +pub(crate) fn fetch_indexes(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + let mut indexes: Vec = key_constraints(link, catalog, table)? + .into_iter() + .map(|(name, kind, columns)| IndexInfo { + name, + keys: columns + .into_iter() + .map(|name| IndexKey::Column { + name, + descending: false, + }) + .collect(), + unique: true, + primary: kind == "PRIMARY KEY", + predicate: None, + }) + .collect(); + let columns: HashSet = fetch_columns(link, catalog, table)? + .into_iter() + .map(|column| column.name) + .collect(); + let rows = text_rows( + link, + // DuckDB lists an index's keys as the text of a list. + "SELECT index_name, CAST(is_unique AS VARCHAR), CAST(is_primary AS VARCHAR), \ + array_to_string(CAST(expressions AS VARCHAR[]), chr(31)) \ + FROM duckdb_indexes() WHERE database_name = ? AND schema_name = ? AND table_name = ? \ + ORDER BY index_name", + &[catalog, schema_of(table), &table.name], + )?; + indexes.extend(rows.iter().map(|row| { + IndexInfo { + name: text(row, 0), + keys: split_names(&text(row, 3)) + .iter() + .map(|key| index_key(key, &columns)) + .collect(), + unique: flag(row, 1), + primary: flag(row, 2), + predicate: None, + } + })); + Ok(indexes) +} + +/// One key of an index: a column where the text names one, an +/// expression otherwise. +fn index_key(text: &str, columns: &HashSet) -> IndexKey { + let trimmed = text.trim(); + let unquoted = trimmed + .strip_prefix('"') + .and_then(|inner| inner.strip_suffix('"')) + .map(|inner| inner.replace("\"\"", "\"")); + let name = unquoted.unwrap_or_else(|| trimmed.to_owned()); + match columns.contains(&name) { + true => IndexKey::Column { + name, + descending: false, + }, + false => IndexKey::Expression { + sql: trimmed.to_owned(), + descending: false, + }, + } +} + +/// Foreign keys: DuckDB keeps the referenced table by name in the same +/// schema, and enforces no action but refusing the change. +pub(crate) fn fetch_foreign_keys( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT constraint_name, referenced_table, \ + array_to_string(constraint_column_names, chr(31)), \ + array_to_string(referenced_column_names, chr(31)) \ + FROM duckdb_constraints() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ? AND constraint_type = 'FOREIGN KEY' \ + ORDER BY constraint_index", + &[catalog, schema_of(table), &table.name], + )?; + let schema = schema_of(table).to_owned(); + Ok(rows + .iter() + .map(|row| ForeignKeyInfo { + name: text(row, 0), + columns: split_names(&text(row, 2)), + ref_schema: Some(schema.clone()), + ref_table: text(row, 1), + ref_columns: split_names(&text(row, 3)), + on_delete: Some(ReferentialAction::NoAction), + on_update: Some(ReferentialAction::NoAction), + }) + .collect()) +} + +fn split_names(joined: &str) -> Vec { + match joined.is_empty() { + true => Vec::new(), + false => joined.split('\u{1f}').map(str::to_owned).collect(), + } +} + +/// The keys of other tables that point at this one. +fn inbound_foreign_keys(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT constraint_name, schema_name, table_name, \ + array_to_string(constraint_column_names, chr(31)), \ + array_to_string(referenced_column_names, chr(31)) \ + FROM duckdb_constraints() \ + WHERE database_name = ? AND schema_name = ? AND referenced_table = ? AND constraint_type = 'FOREIGN KEY' \ + ORDER BY table_name, constraint_index", + &[catalog, schema_of(table), &table.name], + )?; + Ok(rows + .iter() + .map(|row| InboundForeignKey { + name: text(row, 0), + table: TableRef::new(Some(text(row, 1)), text(row, 2)), + columns: split_names(&text(row, 3)), + referenced_columns: split_names(&text(row, 4)), + }) + .collect()) +} + +/// How a row is named again: by the primary key, else by a UNIQUE key +/// whose columns are all NOT NULL. DuckDB's rowid moves when a +/// checkpoint compacts the table, so it names no row. +pub(crate) fn row_identity(link: &Link, catalog: &str, table: &TableRef) -> Result { + let constraints = key_constraints(link, catalog, table)?; + if let Some((_, _, columns)) = constraints.iter().find(|(_, kind, _)| kind == "PRIMARY KEY") { + return Ok(RowIdentity::UniqueKey { + columns: columns.clone(), + }); + } + let not_null: HashSet = fetch_columns(link, catalog, table)? + .into_iter() + .filter(|column| !column.nullable) + .map(|column| column.name) + .collect(); + let unique = constraints + .into_iter() + .filter(|(_, kind, _)| kind == "UNIQUE") + .map(|(_, _, columns)| columns) + .find(|columns| columns.iter().all(|column| not_null.contains(column))); + Ok(match unique { + Some(columns) => RowIdentity::UniqueKey { columns }, + None => RowIdentity::Unordered, + }) +} + +/// DuckDB's own row count for the table, which counts what its storage +/// holds rather than what a query would see. +pub(crate) fn table_estimate(link: &Link, catalog: &str, table: &TableRef) -> Result { + let rows = text_rows( + link, + "SELECT CAST(estimated_size AS VARCHAR) FROM duckdb_tables() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ?", + &[catalog, schema_of(table), &table.name], + )?; + let estimate = rows + .first() + .and_then(|row| row.first().cloned().flatten()) + .and_then(|count| count.parse::().ok()); + Ok(TableEstimate { + rows: estimate.map_or(RowCountEstimate::Unknown, RowCountEstimate::Approximate), + heap_blocks: None, + }) +} + +/// The table as structure changes plan against it. +pub(crate) fn fetch_table_definition( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result { + let create_sql = table_sql(link, catalog, table)?; + let columns = fetch_columns(link, catalog, table)?; + let check_columns = check_constraints(link, catalog, table)?; + let definitions = columns + .into_iter() + .map(|info| { + let generated = info + .is_generated + .then(|| generation_expression(link, catalog, table, &info.name)) + .flatten() + .map(|expression| GeneratedColumn { + expression, + storage: GeneratedStorage::Virtual, + }); + let attributes = ColumnAttributes { + generated, + check: check_columns.get(&info.name).cloned(), + identity: match info.is_auto_increment { + true => IdentityStyle::SequenceDefault, + false => IdentityStyle::None, + }, + ..ColumnAttributes::default() + }; + ColumnDefinition::new(info, attributes) + }) + .collect(); + let indexes = fetch_indexes(link, catalog, table)?; + let source = create_sql.map(|create_statement| TableSource { + create_statement, + dependents: dependent_sources(link, catalog, table).unwrap_or_default(), + }); + Ok(TableDefinition { + table: TableRef::new(Some(schema_of(table).to_owned()), table.name.clone()), + columns: definitions, + primary_key: primary_key(link, catalog, table)?, + indexes, + foreign_keys: fetch_foreign_keys(link, catalog, table)?, + inbound_foreign_keys: inbound_foreign_keys(link, catalog, table)?, + source, + }) +} + +/// A generated column's expression: what DuckDB lists as its default. +fn generation_expression(link: &Link, catalog: &str, table: &TableRef, column: &str) -> Option { + let rows = text_rows( + link, + "SELECT column_default FROM duckdb_columns() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ? AND column_name = ?", + &[catalog, schema_of(table), &table.name, column], + ) + .ok()?; + rows.first() + .and_then(|row| row.first().cloned().flatten()) + .map(SqlExpression::from_catalog_text) +} + +/// The CHECK constraints each column is part of, joined with AND. A +/// CHECK over several columns is every one's: DuckDB refuses a new type +/// for any column a CHECK names, and takes the expression back on the +/// column itself. +fn check_constraints( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT array_to_string(constraint_column_names, chr(31)), expression FROM duckdb_constraints() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ? AND constraint_type = 'CHECK' \ + ORDER BY constraint_index", + &[catalog, schema_of(table), &table.name], + )?; + let mut checks: std::collections::HashMap> = std::collections::HashMap::new(); + for row in &rows { + let Some(expression) = row.get(1).cloned().flatten() else { + continue; + }; + for column in split_names(&text(row, 0)) { + checks.entry(column).or_default().push(expression.clone()); + } + } + Ok(checks + .into_iter() + .map(|(column, expressions)| { + let joined = match expressions.as_slice() { + [only] => only.clone(), + many => many + .iter() + .map(|one| format!("({one})")) + .collect::>() + .join(" AND "), + }; + (column, SqlExpression::from_catalog_text(joined)) + }) + .collect()) +} + +/// The indexes created on the table, which a table rebuilt from its +/// CREATE TABLE text would lose. +fn dependent_sources(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT index_name, sql FROM duckdb_indexes() \ + WHERE database_name = ? AND schema_name = ? AND table_name = ? AND sql IS NOT NULL ORDER BY index_name", + &[catalog, schema_of(table), &table.name], + )?; + Ok(rows + .iter() + .map(|row| SchemaObjectSource { + kind: SchemaObjectKind::Index, + name: text(row, 0), + table: table.name.clone(), + sql: text(row, 1), + }) + .collect()) +} + +#[cfg(test)] +mod tests { + use tablepro_core::column::{ColumnKind, IntegerKind, IntegerWidth}; + + use super::*; + + #[test] + fn a_literal_default_reads_as_its_value() { + let integer = column_type_from_text("INTEGER"); + let text = column_type_from_text("VARCHAR"); + + assert_eq!( + classify_default(Some("42"), &integer), + ColumnDefault::Literal(Value::Int(42)) + ); + assert_eq!( + classify_default(Some("'it''s'"), &text), + ColumnDefault::Literal(Value::Text("it's".to_owned())) + ); + assert_eq!(classify_default(Some("NULL"), &text), ColumnDefault::None); + assert!(matches!( + classify_default(Some("current_timestamp"), &column_type_from_text("TIMESTAMP")), + ColumnDefault::Expression(expression) if expression.as_sql() == "current_timestamp" + )); + assert_eq!(classify_default(None, &integer), ColumnDefault::None); + assert_eq!( + integer.kind, + ColumnKind::Integer(IntegerKind { + width: IntegerWidth::W32, + signed: true + }) + ); + } + + #[test] + fn a_sequence_default_numbers_the_column() { + assert!(is_sequence_default(Some("nextval('s1')"))); + assert!(!is_sequence_default(Some("'nextval'"))); + assert!(!is_sequence_default(None)); + } + + #[test] + fn an_index_key_that_names_a_column_reads_as_one() { + let columns: HashSet = ["note".to_owned(), "Odd Name".to_owned()].into_iter().collect(); + + assert_eq!( + index_key("note", &columns), + IndexKey::Column { + name: "note".to_owned(), + descending: false + } + ); + assert_eq!( + index_key("\"Odd Name\"", &columns), + IndexKey::Column { + name: "Odd Name".to_owned(), + descending: false + } + ); + assert_eq!( + index_key("(lower(note))", &columns), + IndexKey::Expression { + sql: "(lower(note))".to_owned(), + descending: false + } + ); + } +} diff --git a/linux/crates/drivers/duckdb/src/connection.rs b/linux/crates/drivers/duckdb/src/connection.rs new file mode 100644 index 0000000000..33ae1ef91d --- /dev/null +++ b/linux/crates/drivers/duckdb/src/connection.rs @@ -0,0 +1,481 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, PoisonError}; + +use async_trait::async_trait; +use tablepro_core::ddl::TableDefinition; +use tablepro_core::dialect::SqlDialect; +use tablepro_core::meta::{IndexInfo, ObjectInfo, ObjectScope, RowIdentity, SchemaInfo, TableEstimate, TableRef}; +use tablepro_core::statement::Statement; +use tablepro_core::{ + CallOptions, ColumnInfo, CommitResolution, CommitToken, Connection, DriverError, EditorSession, + EditorSessionOptions, FileEndpoint, ForeignKeyInfo, LinkState, LinkWatch, LivenessPolicy, LossPhase, OwnedCalls, + QueryResult, ReadOnlyRefusal, ReadStatement, ResultSink, RowLimit, ServerInfo, SinkFlow, StopRequest, + StreamSummary, TimeoutPhase, WorkspaceSettings, WriteAtomicity, WriteBatch, WriteKind, WriteLedger, WriteReport, +}; +use tokio::time::Instant; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + +use crate::catalog; +use crate::dialect::DuckDbDialect; +use crate::editor_session::DuckDbEditorSession; +use crate::instance::{Instance, Link}; +use crate::pool::{DuckCall, LinkPool, build_pool, probe_timeouts}; +use crate::result_stream::{Events, StreamOutcome, batch, collect, controls_transaction, count, run_bound, run_script}; +use crate::stop::DuckStop; +use crate::write_batch; + +/// The workspace's link to one database file: a pool of connections to +/// the one database instance this process keeps for the file. +pub(crate) struct DuckDbConnection { + /// Let go at close, so the file closes once no editor tab holds it. + instance: Mutex>>, + catalog: String, + pool: LinkPool, + settings: WorkspaceSettings, + liveness: LivenessPolicy, + server_info: ServerInfo, + dialect: Arc, + calls: OwnedCalls, + link: LinkWatch, + closed: AtomicBool, +} + +impl DuckDbConnection { + /// Open the file the endpoint names, creating it first when the user + /// asked for a new database. + pub(crate) async fn open( + endpoint: &FileEndpoint, + settings: WorkspaceSettings, + liveness: LivenessPolicy, + ) -> Result { + let opening = endpoint.clone(); + let read_only = settings.read_only; + let (instance, version) = blocking(move || { + let instance = Instance::open(&opening, read_only)?; + let version = instance + .link()? + .connection + .query_row("SELECT version()", [], |row| row.get::<_, String>(0)) + .map_err(crate::errors::map_error)?; + Ok((instance, version)) + }) + .await?; + let pool = build_pool(&instance, &liveness)?; + Ok(Self { + catalog: instance.catalog().to_owned(), + instance: Mutex::new(Some(instance)), + pool, + settings, + liveness, + server_info: server_info(&version), + dialect: Arc::new(DuckDbDialect::new()), + calls: OwnedCalls::new(liveness.cancel_grace), + link: LinkWatch::new(), + closed: AtomicBool::new(false), + }) + } + + fn ensure_open(&self) -> Result<(), DriverError> { + self.instance().map(drop) + } + + fn instance(&self) -> Result, DriverError> { + let held = self.instance.lock().unwrap_or_else(PoisonError::into_inner).clone(); + match (held, self.closed.load(Ordering::SeqCst)) { + (Some(instance), false) => Ok(instance), + _ => Err(DriverError::ConnectionLost { + during: LossPhase::Idle, + }), + } + } + + /// DuckDB has no statement timeout of its own, so the workspace's + /// is a deadline its interrupt enforces. + fn statement_bounded(&self, options: CallOptions) -> CallOptions { + let bound = Instant::now() + self.settings.statement_timeout; + let deadline = match options.deadline() { + Some(deadline) => deadline.min(bound), + None => bound, + }; + CallOptions::new(Some(deadline), options.cancel().clone()) + } + + /// Run one workspace read on a pool slot, under the call's own stop. + async fn read(&self, options: CallOptions, work: W) -> Result + where + T: Send + 'static, + W: FnOnce(&Link, &str) -> Result + Send + 'static, + { + self.ensure_open()?; + let options = self.statement_bounded(options); + let stop = Arc::new(DuckStop::new()); + let armed = stop.clone(); + let pool = self.pool.clone(); + let timeouts = pool.timeouts(); + let catalog = self.catalog.clone(); + let read = self + .calls + .run_read(options, DuckCall::new(), stop, move |mut call, request| async move { + if let Err(error) = call.acquire(&pool, &timeouts, &request).await { + return (call, Err(error)); + } + armed.arm(call.stop_handle()); + let result = call.run(Some(&request), move |link| work(link, &catalog)).await; + (call, result) + }) + .await; + self.watch(read) + } + + /// A database that hit a fatal error refuses everything after it, + /// so the link is gone for good. + fn watch(&self, result: Result) -> Result { + if matches!(result, Err(DriverError::ConnectionLost { .. })) && !self.closed.load(Ordering::SeqCst) { + self.link.mark_lost(); + } + result + } + + /// A connection of the call's own, for a probe that found every slot + /// busy and for a streamed read. + async fn own_call(&self) -> Result { + let instance = self.instance()?; + blocking(move || instance.link()).await.map(DuckCall::own) + } +} + +fn server_info(version: &str) -> ServerInfo { + let numbers: Vec = version + .trim_start_matches('v') + .split(|c: char| !c.is_ascii_digit()) + .filter_map(|part| part.parse().ok()) + .collect(); + let part = |index: usize| numbers.get(index).copied().unwrap_or(0); + ServerInfo::new("DuckDB", version, part(0), part(1), part(2)) +} + +/// Run DuckDB work that blocks, off the async threads. +pub(crate) async fn blocking(work: W) -> Result +where + T: Send + 'static, + W: FnOnce() -> Result + Send + 'static, +{ + tokio::task::spawn_blocking(work) + .await + .map_err(|error| DriverError::Protocol(format!("DuckDB work stopped: {error}")))? +} + +/// Hand a run's events to `sink` while `produce` makes them on the +/// connection the call holds. A sink that goes stops the run. +pub(crate) async fn stream_into

( + call: &mut DuckCall, + request: Option<&StopRequest>, + sink: &mut dyn ResultSink, + produce: P, +) -> Result +where + P: FnOnce(&Link, &mut Events) -> Result + Send + 'static, +{ + let (sender, mut receiver) = tokio::sync::mpsc::channel(2); + let running = call.run(request, move |link| produce(link, &mut Events::new(sender))); + let forwarding = async move { + while let Some(event) = receiver.recv().await { + if sink.accept(event).await == SinkFlow::ConsumerGone { + return true; + } + } + false + }; + let (outcome, gone) = tokio::join!(running, forwarding); + let mut outcome = outcome?; + outcome.consumer_gone |= gone; + Ok(outcome) +} + +#[async_trait] +impl Connection for DuckDbConnection { + fn server_info(&self) -> &ServerInfo { + &self.server_info + } + + fn dialect(&self) -> Arc { + self.dialect.clone() + } + + fn read_only(&self) -> bool { + self.settings.read_only + } + + fn link_state(&self) -> LinkState { + self.link.state() + } + + fn lost(&self) -> WaitForCancellationFutureOwned { + self.link.lost() + } + + /// With every slot busy, the probe asks on a connection of its own. + async fn probe(&self, options: CallOptions) -> Result<(), DriverError> { + self.ensure_open()?; + let call = match self.pool.timeout_get(&probe_timeouts(&self.liveness)).await { + Ok(object) => DuckCall::pooled(object), + Err(_) => self.own_call().await?, + }; + let stop = Arc::new(DuckStop::new()); + stop.arm(call.stop_handle()); + let probed = self + .calls + .run_read(options, call, stop, |mut call, request| async move { + let result = call.run(Some(&request), |link| batch(link, "SELECT 1")).await; + (call, result) + }) + .await; + let probed = match probed { + Err(DriverError::Timeout { .. }) => Err(DriverError::Timeout { + phase: TimeoutPhase::Probe, + server_cancelled: false, + }), + other => other, + }; + self.watch(probed) + } + + async fn list_schemas(&self, options: CallOptions) -> Result, DriverError> { + self.read(options, |link, catalog| catalog::list_schemas(link, catalog)) + .await + } + + async fn list_objects(&self, scope: &ObjectScope, options: CallOptions) -> Result, DriverError> { + let scope = scope.clone(); + self.read(options, move |link, catalog| { + catalog::list_objects(link, catalog, &scope) + }) + .await + } + + async fn fetch_columns(&self, table: &TableRef, options: CallOptions) -> Result, DriverError> { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::fetch_columns(link, catalog, &table) + }) + .await + } + + async fn fetch_indexes(&self, table: &TableRef, options: CallOptions) -> Result, DriverError> { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::fetch_indexes(link, catalog, &table) + }) + .await + } + + async fn fetch_foreign_keys( + &self, + table: &TableRef, + options: CallOptions, + ) -> Result, DriverError> { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::fetch_foreign_keys(link, catalog, &table) + }) + .await + } + + async fn fetch_table_definition( + &self, + table: &TableRef, + options: CallOptions, + ) -> Result { + let table = table.clone(); + // One transaction, so the parts come from the same moment. + self.read(options, move |link, catalog| { + batch(link, "BEGIN TRANSACTION READ ONLY")?; + let definition = catalog::fetch_table_definition(link, catalog, &table); + link.hold_stops(); + batch(link, "ROLLBACK")?; + definition + }) + .await + } + + async fn row_identity(&self, table: &TableRef, options: CallOptions) -> Result { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::row_identity(link, catalog, &table) + }) + .await + } + + async fn table_estimate(&self, table: &TableRef, options: CallOptions) -> Result { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::table_estimate(link, catalog, &table) + }) + .await + } + + async fn query_bound( + &self, + statement: &Statement, + limit: RowLimit, + options: CallOptions, + ) -> Result { + let statement = statement.clone(); + self.read(options, move |link, _| collect(link, &statement, Some(limit))) + .await + } + + async fn count_rows(&self, statement: &Statement, options: CallOptions) -> Result { + let statement = statement.clone(); + self.read(options, move |link, _| count(link, &statement)).await + } + + /// Every row, on a connection of the read's own, inside a read-only + /// transaction, so user SQL that writes is refused by DuckDB itself. + async fn stream_read( + &self, + read: &ReadStatement, + sink: Box, + options: CallOptions, + ) -> Result { + self.ensure_open()?; + if let ReadStatement::User(sql) = read + && controls_transaction(sql.as_str()) + { + return Err(DriverError::ReadOnly(ReadOnlyRefusal::ClientGuard)); + } + let call = self.own_call().await?; + let stop = Arc::new(DuckStop::new()); + stop.arm(call.stop_handle()); + let read = read.clone(); + let streamed = self + .calls + .run_read(options, call, stop, move |mut call, request| async move { + let mut sink = sink; + let result = stream_into(&mut call, Some(&request), sink.as_mut(), move |link, events| { + batch(link, "BEGIN TRANSACTION READ ONLY")?; + let streamed = match &read { + ReadStatement::Generated(statement) => run_bound(link, statement, events), + ReadStatement::User(sql) => run_script(link, sql.as_str(), None, events, &|| false), + }; + link.hold_stops(); + let _ = batch(link, "ROLLBACK"); + streamed + }) + .await; + (call, result) + }) + .await; + let streamed = self.watch(streamed)?; + // The sink stopped reading, so the read did not run to its end. + if streamed.consumer_gone { + return Err(DriverError::Cancelled); + } + Ok(StreamSummary::new(streamed.rows)) + } + + async fn write_batch(&self, batch: &WriteBatch, options: CallOptions) -> Result { + if self.settings.read_only { + return Err(DriverError::ReadOnly(ReadOnlyRefusal::ClientGuard)); + } + if batch.is_empty() { + return Ok(WriteReport::default()); + } + batch.foreign_keys.require_enforced()?; + self.ensure_open()?; + // A schema change runs as long as it takes: one stopped halfway + // by the statement timeout would only roll back. + let options = match batch.kind { + WriteKind::RowChanges => self.statement_bounded(options), + WriteKind::SchemaChanges => options, + }; + let ledger = Arc::new(WriteLedger::new(WriteAtomicity::Transactional)); + let recorded = ledger.clone(); + let stop = Arc::new(DuckStop::new()); + let armed = stop.clone(); + let pool = self.pool.clone(); + let timeouts = pool.timeouts(); + let batch = batch.clone(); + let written = self + .calls + .run_write( + options, + DuckCall::new(), + stop, + ledger, + move |mut call, request| async move { + if let Err(error) = call.acquire(&pool, &timeouts, &request).await { + return (call, Err(error)); + } + armed.arm(call.stop_handle()); + let stopping = request.clone(); + let outcome = call + .run(Some(&request), move |link| { + Ok(write_batch::run(link, &batch, &recorded, &stopping)) + }) + .await; + let result = match outcome { + Ok((result, reusable)) => { + if !reusable { + call.taint(); + } + result + } + Err(error) => Err(error), + }; + (call, result) + }, + ) + .await; + self.watch(written) + } + + /// DuckDB commits on the local file, so a COMMIT never goes out over + /// a link that can fail, and there is no token to ask about. + async fn resolve_commit( + &self, + _token: &CommitToken, + _options: CallOptions, + ) -> Result { + self.ensure_open()?; + Ok(CommitResolution::Unresolvable) + } + + async fn open_editor_session( + &self, + options: EditorSessionOptions, + cancel: CancellationToken, + ) -> Result, DriverError> { + let session = DuckDbEditorSession::open(self.instance()?, options, &self.liveness, cancel).await?; + Ok(Arc::new(session)) + } + + async fn close(&self) -> Result<(), DriverError> { + self.closed.store(true, Ordering::SeqCst); + self.calls.shutdown().await; + self.pool.close(); + let instance = self.instance.lock().unwrap_or_else(PoisonError::into_inner).take(); + // DuckDB closes the file as the last connection to it goes, which + // checkpoints it: not on an async thread. + if let Some(instance) = instance { + let _ = blocking(move || { + drop(instance); + Ok(()) + }) + .await; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_version_reads_as_its_parts() { + let info = server_info("v1.5.6"); + + assert_eq!(info, ServerInfo::new("DuckDB", "v1.5.6", 1, 5, 6)); + } +} diff --git a/linux/crates/drivers/duckdb/src/create_file.rs b/linux/crates/drivers/duckdb/src/create_file.rs new file mode 100644 index 0000000000..1a0b3e115e --- /dev/null +++ b/linux/crates/drivers/duckdb/src/create_file.rs @@ -0,0 +1,230 @@ +use std::ffi::OsStr; +use std::fs::{File, OpenOptions}; +use std::io::ErrorKind; +use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::path::{Path, PathBuf}; + +use rustix::fs::FlockOperation; +use rustix::io::Errno; +use tablepro_core::{BusyReason, DriverError}; + +use crate::errors::map_error; +use crate::instance::file_error; + +/// The write-ahead log DuckDB keeps beside a database. One left from the +/// database being replaced would be replayed into the new one. +const WAL_SUFFIX: &str = ".wal"; + +/// Put an empty database at `path`, replacing whatever is there. +/// +/// The save dialog has already asked before replacing a file. An empty +/// file is no DuckDB database, so the new one is written beside the +/// target first, then renamed over it, which is atomic: a reader sees the +/// old file or the new one, never half of one. +pub(crate) fn create_or_replace(path: &Path) -> Result<(), DriverError> { + let parent = match path.parent() { + Some(parent) if !parent.as_os_str().is_empty() => parent, + _ => { + return Err(DriverError::FileNotFound { + path: path.to_path_buf(), + }); + } + }; + let Some(name) = path.file_name() else { + return Err(DriverError::NotADatabase { + path: path.to_path_buf(), + }); + }; + if !parent.is_dir() { + return Err(DriverError::FileNotFound { + path: path.to_path_buf(), + }); + } + let temporary = beside(parent, name, "tmp"); + let placed = build_empty(&temporary, path).and_then(|()| place(&temporary, path, parent, name)); + if placed.is_err() { + // Nothing half-built is left beside the user's files. + let _ = std::fs::remove_file(&temporary); + let _ = std::fs::remove_file(wal_of(&temporary)); + return placed; + } + // The rename is durable only once the directory that records it is. + File::open(parent) + .and_then(|directory| directory.sync_all()) + .map_err(|error| file_error(error.kind(), path)) +} + +/// A hidden name beside the user's file, unique to this call. +fn beside(parent: &Path, name: &OsStr, suffix: &str) -> PathBuf { + parent.join(format!( + ".{}.{}.{suffix}", + name.to_string_lossy(), + uuid::Uuid::new_v4().simple() + )) +} + +/// An empty database, checkpointed into the file so no log goes with +/// it, readable by its owner only. +fn build_empty(temporary: &Path, target: &Path) -> Result<(), DriverError> { + let connection = duckdb::Connection::open(temporary).map_err(map_error)?; + connection.execute_batch("CHECKPOINT").map_err(map_error)?; + connection.close().map_err(|(_, error)| map_error(error))?; + std::fs::set_permissions(temporary, std::fs::Permissions::from_mode(0o600)) + .and_then(|()| File::open(temporary)?.sync_all()) + .map_err(|error| file_error(error.kind(), target)) +} + +/// Rename the new database over `path`. The old file's log is moved +/// aside first and put back if the rename fails, so a database that +/// stays keeps the changes only its log holds. +fn place(temporary: &Path, path: &Path, parent: &Path, name: &OsStr) -> Result<(), DriverError> { + let _held = lock_replaced(path)?; + let wal = wal_of(path); + let aside = beside(parent, name, "wal.tmp"); + let moved = match std::fs::rename(&wal, &aside) { + Ok(()) => true, + Err(error) if error.kind() == ErrorKind::NotFound => false, + Err(error) => return Err(file_error(error.kind(), path)), + }; + if let Err(error) = std::fs::rename(temporary, path) { + if moved && let Err(restoring) = std::fs::rename(&aside, &wal) { + tracing::warn!(error = %restoring, aside = %aside.display(), "could not put a database's log back"); + } + return Err(file_error(error.kind(), path)); + } + if moved && let Err(error) = std::fs::remove_file(&aside) { + tracing::warn!(%error, aside = %aside.display(), "could not remove a replaced database's log"); + } + Ok(()) +} + +/// Lock the file being replaced as DuckDB locks a database it opens, and +/// hold the lock through the rename. Another process with the database +/// open holds that lock, and would go on writing a log beside the new +/// file under the same name. +fn lock_replaced(path: &Path) -> Result, DriverError> { + let metadata = match std::fs::metadata(path) { + Ok(metadata) if metadata.is_file() => metadata, + // Nothing there to lock, or something the rename itself refuses. + Ok(_) => return Ok(None), + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(file_error(error.kind(), path)), + }; + // A POSIX lock belongs to the process, and closing any descriptor on + // the file drops all of them, so a file this process holds open is + // not opened again here. + if held_by_this_process(&metadata) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + let (file, lock) = match OpenOptions::new().read(true).write(true).open(path) { + Ok(file) => (file, FlockOperation::NonBlockingLockExclusive), + // A write lock needs the file open for writing. A shared lock + // still meets the writer's. + Err(error) if error.kind() == ErrorKind::PermissionDenied || error.kind() == ErrorKind::ReadOnlyFilesystem => ( + File::open(path).map_err(|error| file_error(error.kind(), path))?, + FlockOperation::NonBlockingLockShared, + ), + Err(error) => return Err(file_error(error.kind(), path)), + }; + match rustix::fs::fcntl_lock(&file, lock) { + Ok(()) => Ok(Some(file)), + Err(errno) if errno == Errno::AGAIN || errno == Errno::ACCESS => Err(DriverError::Busy(BusyReason::FileLocked)), + // A file system without locks: DuckDB could not have locked it + // either. + Err(_) => Ok(Some(file)), + } +} + +/// Whether a descriptor this process has open names the file. +fn held_by_this_process(file: &std::fs::Metadata) -> bool { + let Ok(descriptors) = std::fs::read_dir("/proc/self/fd") else { + return false; + }; + descriptors.flatten().any(|descriptor| { + std::fs::metadata(descriptor.path()).is_ok_and(|held| held.dev() == file.dev() && held.ino() == file.ino()) + }) +} + +fn wal_of(path: &Path) -> PathBuf { + let mut wal = path.as_os_str().to_owned(); + wal.push(WAL_SUFFIX); + PathBuf::from(wal) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_existing_file_and_its_log_are_replaced_by_an_empty_private_database() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("data.duckdb"); + std::fs::write(&path, b"old contents").unwrap(); + std::fs::write(wal_of(&path), b"old log").unwrap(); + + create_or_replace(&path).unwrap(); + + let metadata = std::fs::metadata(&path).unwrap(); + assert_eq!(metadata.permissions().mode() & 0o777, 0o600); + assert!( + !wal_of(&path).exists(), + "the old log would be replayed into the new database" + ); + let leftovers: Vec<_> = std::fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + assert_eq!(leftovers, [std::ffi::OsString::from("data.duckdb")]); + let reopened = duckdb::Connection::open(&path).unwrap(); + let tables: i64 = reopened + .query_row("SELECT count(*) FROM duckdb_tables()", [], |row| row.get(0)) + .unwrap(); + assert_eq!(tables, 0); + } + + #[test] + fn the_log_of_a_database_that_stays_is_kept() { + let directory = tempfile::tempdir().unwrap(); + // A directory cannot be renamed over, so the database stays. + let path = directory.path().join("data.duckdb"); + std::fs::create_dir(&path).unwrap(); + std::fs::write(path.join("inside"), b"keeps the directory from being replaced").unwrap(); + std::fs::write(wal_of(&path), b"committed changes").unwrap(); + + assert!(create_or_replace(&path).is_err()); + + assert_eq!(std::fs::read(wal_of(&path)).unwrap(), b"committed changes"); + let mut leftovers: Vec<_> = std::fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + leftovers.sort(); + assert_eq!( + leftovers, + [ + std::ffi::OsString::from("data.duckdb"), + std::ffi::OsString::from("data.duckdb.wal") + ] + ); + } + + #[test] + fn a_file_this_process_holds_is_not_replaced() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("held.duckdb"); + std::fs::write(&path, b"open elsewhere in this process").unwrap(); + let open = File::open(&path).unwrap(); + + assert_eq!(create_or_replace(&path), Err(DriverError::Busy(BusyReason::FileLocked))); + assert_eq!(std::fs::read(&path).unwrap(), b"open elsewhere in this process"); + drop(open); + } + + #[test] + fn a_missing_directory_is_file_not_found() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("missing").join("data.duckdb"); + + assert_eq!(create_or_replace(&path), Err(DriverError::FileNotFound { path })); + } +} diff --git a/linux/crates/drivers/duckdb/src/ddl.rs b/linux/crates/drivers/duckdb/src/ddl.rs new file mode 100644 index 0000000000..c7950c6ab0 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/ddl.rs @@ -0,0 +1,412 @@ +//! Structure changes on DuckDB. +//! +//! DuckDB takes one action per ALTER TABLE, and a table something else +//! depends on, an index made with CREATE INDEX or another table's +//! foreign key, takes only ADD COLUMN and a column's default: every +//! other change refuses with a dependency error. A column refuses a new +//! type while a key, a CHECK, an index or a generated column uses it. +//! The plans offer only what DuckDB 1.5 takes, measured against it in +//! the driver's DDL tests. + +use tablepro_core::column::ColumnType; +use tablepro_core::ddl::ddl_sql::{column_list, create_index_sql, default_sql, foreign_key_sql}; +use tablepro_core::ddl::{ + BuildDdlError, ColumnCapabilities, ColumnDefinition, DdlCapabilities, DdlDialect, DdlOperation, DdlPlan, + DraftColumn, Permission, ReferentialAction, RefusalReason, TableChanges, TableDefinition, TableDraft, +}; +use tablepro_core::dialect::SqlDialect; +use tablepro_core::meta::{IndexInfo, TableRef}; +use tablepro_core::value::Value; + +use crate::dialect::DuckDbDialect; + +static CAPABILITIES: DdlCapabilities = DdlCapabilities { + create_table: true, + rename_table: true, + add_column: true, + drop_column: true, + rename_column: true, + alter_column_type: true, + alter_column_nullability: true, + alter_column_default: true, + // A key can be added to a table that has none; DuckDB drops or + // moves none. + alter_primary_key: true, + alter_auto_increment: false, + auto_increment_outside_primary_key: false, + create_index: true, + drop_index: true, + expression_index_keys: true, + // DuckDB takes DESC in CREATE INDEX and keeps the key ascending. + descending_index_keys: false, + partial_indexes: false, + add_foreign_key: false, + drop_foreign_key: false, + column_comments: true, + // CASCADE drops no table whose key another table references. + drop_table_cascade: false, +}; + +const TYPE_SUGGESTIONS: &[&str] = &[ + "VARCHAR", + "INTEGER", + "BIGINT", + "DOUBLE", + "DECIMAL(18,3)", + "BOOLEAN", + "DATE", + "TIMESTAMP", + "TIMESTAMPTZ", + "TIME", + "INTERVAL", + "UUID", + "BLOB", + "JSON", + "HUGEINT", +]; + +/// What holds the table in place: the indexes made on it and the keys +/// of other tables that reference it. +fn dependents(table: &TableDefinition) -> Vec { + let created_indexes = table.indexes.iter().filter(|index| { + let constraint = table + .primary_key + .as_ref() + .is_some_and(|key| key.name.as_deref() == Some(index.name.as_str())) + || index.primary; + let created = table + .source + .as_ref() + .is_some_and(|source| source.dependents.iter().any(|dependent| dependent.name == index.name)); + created && !constraint + }); + created_indexes + .map(|index| index.name.clone()) + .chain(table.inbound_foreign_keys.iter().map(|key| key.name.clone())) + .collect() +} + +/// What uses one column so that DuckDB will not change its type: a key +/// or index over it, a foreign key from it, a generated column on it. +fn type_users(table: &TableDefinition, column: &ColumnDefinition) -> Vec { + let name = column.name(); + let indexes = table + .indexes + .iter() + .filter(|index| index.keys.iter().any(|key| key.column_name() == Some(name))) + .map(|index| index.name.clone()); + let keys = table + .foreign_keys + .iter() + .filter(|key| key.columns.iter().any(|column| column == name)) + .map(|key| key.name.clone()); + let generated = table + .columns + .iter() + .filter(|other| { + other + .attributes + .generated + .as_ref() + .is_some_and(|generated| mentions(generated.expression.as_sql(), name)) + }) + .map(|other| other.name().to_owned()); + indexes.chain(keys).chain(generated).collect() +} + +/// Whether an expression names the column, as a word or quoted. +fn mentions(expression: &str, column: &str) -> bool { + let quoted = format!("\"{}\"", column.replace('"', "\"\"")); + expression.contains("ed) + || expression + .split(|c: char| !(c.is_alphanumeric() || c == '_')) + .any(|word| word.eq_ignore_ascii_case(column)) +} + +impl DdlDialect for DuckDbDialect { + fn capabilities(&self) -> &DdlCapabilities { + &CAPABILITIES + } + + fn sql_dialect(&self) -> &dyn SqlDialect { + self + } + + /// A table something depends on can be neither renamed nor given a + /// key, and a key goes only on a table with none. + fn table_capabilities(&self, table: &TableDefinition) -> DdlCapabilities { + let free = dependents(table).is_empty(); + DdlCapabilities { + rename_table: free, + alter_primary_key: free && table.primary_key.is_none(), + ..CAPABILITIES + } + } + + fn column_capabilities(&self, table: &TableDefinition, column: &ColumnDefinition) -> ColumnCapabilities { + let mut permissions = ColumnCapabilities::standard(&self.table_capabilities(table), table, column); + let held = dependents(table); + if !held.is_empty() { + let in_use = || Permission::Refused(RefusalReason::ColumnInUse(held.clone())); + permissions.rename = permissions.rename.and(in_use); + permissions.drop = permissions.drop.and(in_use); + permissions.alter_type = permissions.alter_type.and(in_use); + permissions.alter_nullability = permissions.alter_nullability.and(in_use); + } + let users = type_users(table, column); + if !users.is_empty() { + let in_use = || Permission::Refused(RefusalReason::ColumnInUse(users.clone())); + permissions.alter_type = permissions.alter_type.and(in_use); + permissions.drop = permissions.drop.and(in_use); + } + if let Some(check) = &column.attributes.check { + let checked = || Permission::Refused(RefusalReason::ConflictingAttribute("CHECK constraint".to_owned())); + permissions.alter_type = permissions.alter_type.and(checked); + // DuckDB drops a CHECK on the column alone with it, and + // refuses to drop a column a CHECK shares with another. + let shared = table + .columns + .iter() + .any(|other| other.name() != column.name() && mentions(check.as_sql(), other.name())); + if shared { + permissions.drop = permissions.drop.and(checked); + } + } + let referencing: Vec = table + .foreign_keys + .iter() + .filter(|key| key.columns.iter().any(|name| name == column.name())) + .map(|key| key.name.clone()) + .collect(); + if !referencing.is_empty() { + let in_use = || Permission::Refused(RefusalReason::ColumnInUse(referencing.clone())); + permissions.rename = permissions.rename.and(in_use); + } + permissions + } + + /// DuckDB drops an index made with CREATE INDEX, and no constraint: + /// a UNIQUE key's index goes only with its table. + fn index_removal(&self, table: &TableDefinition, index: &IndexInfo) -> Permission { + let created = table + .source + .as_ref() + .is_some_and(|source| source.dependents.iter().any(|dependent| dependent.name == index.name)); + Permission::unless(created, RefusalReason::EngineUnsupported) + } + + fn type_suggestions(&self) -> &'static [&'static str] { + TYPE_SUGGESTIONS + } + + fn default_column_type(&self) -> &'static str { + "VARCHAR" + } + + fn referential_actions(&self) -> &'static [ReferentialAction] { + &[ReferentialAction::NoAction] + } + + /// DuckDB keeps a foreign key's table by name alone, in the schema + /// of the table that holds the key. + fn references_other_schemas(&self) -> bool { + false + } + + fn plan_create_table(&self, draft: &TableDraft) -> Result { + draft.check_create(self)?; + let table = TableRef::new(draft.table.schema.clone(), draft.table.name.trim()); + let mut definitions = draft + .columns + .iter() + .map(|column| self.column_definition(column, true)) + .collect::, _>>()?; + let key: Vec = draft.primary_key().into_iter().map(str::to_owned).collect(); + if !key.is_empty() { + definitions.push(format!("PRIMARY KEY ({})", column_list(self, &key))); + } + definitions.extend(draft.foreign_keys.iter().map(|key| foreign_key_sql(self, &table, key))); + let mut statements = vec![format!( + "CREATE TABLE {} (\n {}\n)", + self.qualify(&table), + definitions.join(",\n ") + )]; + for column in &draft.columns { + if let Some(comment) = column.comment_text() { + statements.push(self.comment_on(&table, &column.name, Some(comment))); + } + } + statements.extend( + draft + .indexes + .iter() + .filter(|index| !index.primary) + .map(|index| create_index_sql(self, &table, index)), + ); + Ok(DdlPlan::from_sql(statements)) + } + + fn plan_alter_table(&self, original: &TableDefinition, changes: &TableChanges) -> Result { + changes.check(self, original)?; + let narrowed = self.table_capabilities(original); + if changes.renamed_table.is_some() && !narrowed.rename_table { + return Err(refused("", DdlOperation::RenameTable, dependents(original))); + } + let mut statements = Vec::new(); + if let Some(name) = &changes.renamed_table { + statements.push(format!( + "ALTER TABLE {} RENAME TO {}", + self.qualify(&original.table), + self.quote_identifier(name) + )); + } + let table = changes.target(&original.table); + let qualified = self.qualify(&table); + let alter = |action: String| format!("ALTER TABLE {qualified} {action}"); + for index in &changes.dropped_indexes { + let index = TableRef::new(table.schema.clone(), index.name.clone()); + statements.push(format!("DROP INDEX {}", self.qualify(&index))); + } + for column in &changes.dropped_columns { + statements.push(alter(format!("DROP COLUMN {}", self.quote_identifier(column.name())))); + } + for rename in changes.ordered_renames(original) { + statements.push(alter(format!( + "RENAME COLUMN {} TO {}", + self.quote_identifier(&rename.from), + self.quote_identifier(&rename.to) + ))); + } + for alteration in &changes.altered_columns { + let draft = &alteration.draft; + let name = self.quote_identifier(&draft.name); + let changed = alteration.changed; + if changed.column_type { + let resolved = draft.column_type.resolve(self, &draft.name)?; + statements.push(alter(format!("ALTER COLUMN {name} TYPE {}", resolved.sql))); + } + if changed.nullability { + let action = if draft.nullable { + "DROP NOT NULL" + } else { + "SET NOT NULL" + }; + statements.push(alter(format!("ALTER COLUMN {name} {action}"))); + } + if changed.default { + let column_type = draft.column_type.column_type(self, &draft.name)?; + statements.push(alter( + match default_sql(self, &draft.name, &draft.default, &column_type)? { + Some(default) => format!("ALTER COLUMN {name} SET DEFAULT {default}"), + None => format!("ALTER COLUMN {name} DROP DEFAULT"), + }, + )); + } + if changed.comment { + statements.push(self.comment_on(&table, &draft.name, draft.comment_text())); + } + } + let held = dependents(original); + for column in &changes.added_columns { + // DuckDB adds no column with a constraint, so NOT NULL is set + // once it is there, which a table with dependents refuses. + if !column.nullable && !held.is_empty() { + return Err(refused(&column.name, DdlOperation::AddColumn, held)); + } + statements.push(alter(format!("ADD COLUMN {}", self.column_definition(column, false)?))); + if !column.nullable { + statements.push(alter(format!( + "ALTER COLUMN {} SET NOT NULL", + self.quote_identifier(&column.name) + ))); + } + if let Some(comment) = column.comment_text() { + statements.push(self.comment_on(&table, &column.name, Some(comment))); + } + } + if let Some(change) = &changes.primary_key { + if change.from.is_some() || !narrowed.alter_primary_key { + let column = change.to.first().map_or("", String::as_str); + return Err(refused(column, DdlOperation::AlterPrimaryKey, held)); + } + if !change.to.is_empty() { + statements.push(alter(format!("ADD PRIMARY KEY ({})", column_list(self, &change.to)))); + } + } + statements.extend( + changes + .added_indexes + .iter() + .map(|index| create_index_sql(self, &table, index)), + ); + DdlPlan::for_changes(changes, statements) + } + + /// DuckDB's own CREATE TABLE text, which keeps its CHECK and UNIQUE + /// constraints and its generated columns, then the indexes made on + /// the table and the column comments, which that text leaves out. + fn create_table_script(&self, definition: &TableDefinition) -> Result { + let source = definition.source.as_ref().ok_or(BuildDdlError::NoTableSource)?; + let mut statements = vec![source.create_statement.trim().trim_end_matches(';').to_owned()]; + statements.extend( + source + .dependents + .iter() + .map(|dependent| dependent.sql.trim().trim_end_matches(';').to_owned()), + ); + for column in &definition.columns { + if let Some(comment) = column.info.comment.as_deref().filter(|text| !text.is_empty()) { + statements.push(self.comment_on(&definition.table, column.name(), Some(comment))); + } + } + Ok(DdlPlan::from_sql(statements)) + } +} + +fn refused(column: &str, operation: DdlOperation, users: Vec) -> BuildDdlError { + BuildDdlError::Refused { + column: column.to_owned(), + operation, + reason: match users.is_empty() { + true => RefusalReason::EngineUnsupported, + false => RefusalReason::ColumnInUse(users), + }, + } +} + +impl DuckDbDialect { + /// A column as CREATE TABLE declares it, or as ADD COLUMN does when + /// `constraints` is off. A column read from the catalogue keeps its + /// generation expression and its CHECK. + fn column_definition(&self, column: &DraftColumn, constraints: bool) -> Result { + let resolved = column.column_type.resolve(self, &column.name)?; + let attributes = column.original.as_ref().map(|original| &original.attributes); + let mut parts = vec![self.quote_identifier(&column.name), resolved.sql.clone()]; + if let Some(generated) = attributes.and_then(|attributes| attributes.generated.as_ref()) { + parts.push(format!( + "GENERATED ALWAYS AS ({}) VIRTUAL", + generated.expression.as_sql() + )); + } else if let Some(default) = default_sql(self, &column.name, &column.default, &resolved.column_type)? { + parts.push(format!("DEFAULT {default}")); + } + if constraints { + if !column.nullable { + parts.push("NOT NULL".to_owned()); + } + if let Some(check) = attributes.and_then(|attributes| attributes.check.as_ref()) { + parts.push(format!("CHECK ({})", check.as_sql())); + } + } + Ok(parts.join(" ")) + } + + fn comment_on(&self, table: &TableRef, column: &str, comment: Option<&str>) -> String { + let target = format!("{}.{}", self.qualify(table), self.quote_identifier(column)); + let text = comment.map(|text| Value::Text(text.to_owned())); + match text.and_then(|text| SqlDialect::literal(self, &text, &ColumnType::untyped_text("VARCHAR")).ok()) { + Some(literal) => format!("COMMENT ON COLUMN {target} IS {literal}"), + None => format!("COMMENT ON COLUMN {target} IS NULL"), + } + } +} diff --git a/linux/crates/drivers/duckdb/src/decode/bits.rs b/linux/crates/drivers/duckdb/src/decode/bits.rs new file mode 100644 index 0000000000..914ed3813d --- /dev/null +++ b/linux/crates/drivers/duckdb/src/decode/bits.rs @@ -0,0 +1,49 @@ +use tablepro_core::value::BitString; + +/// A BIT as DuckDB stores it: a byte that says how many bits of the next +/// byte are padding, then the bits, first bit highest. +pub(crate) fn bit_string(stored: &[u8]) -> Option { + let (&padding, data) = stored.split_first()?; + if padding > 7 || (data.is_empty() && padding != 0) { + return None; + } + let padding = u32::from(padding); + let bit_len = u32::try_from(data.len()).ok()?.checked_mul(8)?.checked_sub(padding)?; + let mut packed = vec![0u8; usize::try_from(bit_len.div_ceil(8)).ok()?]; + for index in 0..bit_len { + let source = index + padding; + let byte = data.get(usize::try_from(source / 8).ok()?)?; + if byte & (0x80 >> (source % 8)) != 0 { + let target = packed.get_mut(usize::try_from(index / 8).ok()?)?; + *target |= 0x80 >> (index % 8); + } + } + BitString::from_bytes(bit_len, packed).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn padding_bits_are_dropped_from_the_front() { + // '10101': three bits of padding, then 10101. + let bits = bit_string(&[3, 0b1111_0101]).unwrap(); + + assert_eq!(bits.to_string(), "10101"); + assert_eq!(bits.bit_len(), 5); + } + + #[test] + fn a_bit_string_can_span_bytes() { + let bits = bit_string(&[6, 0b1111_1110, 0b1010_1010]).unwrap(); + + assert_eq!(bits.to_string(), "1010101010"); + } + + #[test] + fn a_malformed_bit_string_is_refused() { + assert_eq!(bit_string(&[]), None); + assert_eq!(bit_string(&[9, 0]), None); + } +} diff --git a/linux/crates/drivers/duckdb/src/decode/mod.rs b/linux/crates/drivers/duckdb/src/decode/mod.rs new file mode 100644 index 0000000000..939968efbf --- /dev/null +++ b/linux/crates/drivers/duckdb/src/decode/mod.rs @@ -0,0 +1,293 @@ +//! Values off DuckDB's results. +//! +//! DuckDB hands a result over as Arrow arrays, whose types say less +//! than DuckDB's own: HUGEINT, UHUGEINT and DECIMAL(38,0) arrive alike, +//! as do VARCHAR, JSON and UUID. Each value is read by the column's +//! DuckDB type, and one that Arrow cannot carry whole reads as +//! unreadable rather than as a wrong value. + +mod bits; +mod nested; +mod text; + +use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Offset, Utc}; +use duckdb::arrow::array::{Array, AsArray}; +use duckdb::arrow::datatypes::{ + DataType, Date32Type, Decimal32Type, Decimal64Type, Decimal128Type, Float32Type, Float64Type, Int8Type, Int16Type, + Int32Type, Int64Type, IntervalMonthDayNanoType, Time64MicrosecondType, Time64NanosecondType, TimeUnit, + TimestampMicrosecondType, TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, UInt8Type, + UInt16Type, UInt32Type, UInt64Type, +}; +use tablepro_core::column::IntegerWidth; +use tablepro_core::value::{ + JsonText, OffsetTimestamp, OtherValue, SqlDecimal, SqlInterval, SqlTime, Temporal, UndecodableReason, + UndecodedValue, Value, WideInteger, +}; + +use crate::duck_type::DuckType; + +pub(crate) use self::bits::bit_string; +pub(crate) use self::text::interval_text; + +/// DuckDB's infinite DATE, one either side of every finite one. +const DATE_INFINITY: i32 = i32::MAX; +/// DuckDB's infinite TIMESTAMP, in every unit. +const TIMESTAMP_INFINITY: i64 = i64::MAX; +const DAYS_TO_UNIX_EPOCH_FROM_CE: i32 = 719_163; + +/// The value at `row` of a column of type `duck`. +pub(crate) fn value_at(array: &dyn Array, duck: &DuckType, row: usize) -> Value { + if array.is_null(row) { + return Value::Null; + } + if duck.is_nested() { + return match nested::text(array, duck, row) { + Some(text) => Value::Other(Box::new(OtherValue { + type_name: duck.name(), + text, + })), + None => unreadable(duck, UndecodableReason::UnsupportedType), + }; + } + scalar(array, duck, row).unwrap_or_else(|reason| unreadable(duck, reason)) +} + +pub(crate) fn unreadable(duck: &DuckType, reason: UndecodableReason) -> Value { + Value::Undecodable(Box::new(UndecodedValue { + type_name: duck.name(), + reason, + })) +} + +type Read = Result; + +fn scalar(array: &dyn Array, duck: &DuckType, row: usize) -> Read { + Ok(match duck { + DuckType::Boolean => Value::Bool(array.as_boolean_opt().ok_or(mismatch())?.value(row)), + DuckType::Integer { width, signed } => integer(array, *width, *signed, row)?, + DuckType::Float => Value::Float32(primitive::(array, row)?), + DuckType::Double => Value::Float64(primitive::(array, row)?), + DuckType::Decimal { scale, .. } => Value::Decimal(SqlDecimal::from_scaled_i128( + decimal_mantissa(array, row)?, + u32::from(*scale), + )), + DuckType::Varchar | DuckType::Enum => Value::Text(string(array, row)?.to_owned()), + DuckType::Json => { + let text = string(array, row)?.to_owned(); + match JsonText::parse(text.clone()) { + Ok(json) => Value::Json(json), + Err(_) => Value::Text(text), + } + } + DuckType::Blob | DuckType::Geometry => Value::Bytes(bytes(array, row)?.to_vec()), + DuckType::Uuid => Value::Uuid(uuid(array, row)?), + DuckType::Bit => Value::Bits(bit_string(bytes(array, row)?).ok_or(UndecodableReason::InvalidEncoding)?), + DuckType::BigNum => big_number(array, row)?, + DuckType::Date => Value::Date(date(primitive::(array, row)?)?), + DuckType::Time => Value::Time(time_of_day(array, row)?), + DuckType::TimeNs => Value::Time(time_of_day(array, row)?), + DuckType::Timestamp(_) => Value::Timestamp(timestamp(array, row)?), + DuckType::TimestampTz => Value::TimestampTz(match timestamp(array, row)? { + Temporal::Finite(utc) => Temporal::Finite(OffsetTimestamp::from_datetime( + utc.and_utc().with_timezone(&utc_offset()), + )), + Temporal::Infinity => Temporal::Infinity, + Temporal::NegInfinity => Temporal::NegInfinity, + }), + DuckType::Interval => { + let interval = primitive::(array, row)?; + Value::Interval(SqlInterval { + months: interval.months, + days: interval.days, + microseconds: interval_micros(interval.nanoseconds).ok_or(UndecodableReason::OutOfRange)?, + }) + } + DuckType::Null => Value::Null, + // Arrow carries a TIME WITH TIME ZONE as its time alone, so the + // offset would be lost and the value read as another one. + DuckType::TimeTz | DuckType::Variant | DuckType::Other(_) => return Err(UndecodableReason::UnsupportedType), + DuckType::List(_) | DuckType::Array(..) | DuckType::Struct(_) | DuckType::Map(..) | DuckType::Union(_) => { + return Err(UndecodableReason::UnsupportedType); + } + }) +} + +/// The microseconds of an interval DuckDB handed over as nanoseconds. +/// DuckDB multiplies its microseconds by 1,000 in an `i64` on the way to +/// Arrow, which wraps for a span past about 292 years; a wrapped value is +/// no whole number of microseconds, and one that is cannot be told apart. +pub(crate) fn interval_micros(nanoseconds: i64) -> Option { + (nanoseconds % 1_000 == 0).then_some(nanoseconds / 1_000) +} + +fn mismatch() -> UndecodableReason { + UndecodableReason::InvalidEncoding +} + +fn primitive(array: &dyn Array, row: usize) -> Read { + array + .as_primitive_opt::() + .map(|values| values.value(row)) + .ok_or(mismatch()) +} + +fn integer(array: &dyn Array, width: IntegerWidth, signed: bool, row: usize) -> Read { + Ok(match (width, signed) { + (IntegerWidth::W8, true) => Value::Int(i64::from(primitive::(array, row)?)), + (IntegerWidth::W16, true) => Value::Int(i64::from(primitive::(array, row)?)), + (IntegerWidth::W24 | IntegerWidth::W32, true) => Value::Int(i64::from(primitive::(array, row)?)), + (IntegerWidth::W64, true) => Value::Int(primitive::(array, row)?), + (IntegerWidth::W8, false) => Value::UInt(u64::from(primitive::(array, row)?)), + (IntegerWidth::W16, false) => Value::UInt(u64::from(primitive::(array, row)?)), + (IntegerWidth::W24 | IntegerWidth::W32, false) => Value::UInt(u64::from(primitive::(array, row)?)), + (IntegerWidth::W64, false) => Value::UInt(primitive::(array, row)?), + (IntegerWidth::W128 | IntegerWidth::W256, true) => { + Value::WideInt(WideInteger::from_i128(primitive::(array, row)?)) + } + // A UHUGEINT travels in a 128-bit decimal's bits, so its sign bit is + // the top bit of the number. + (IntegerWidth::W128 | IntegerWidth::W256, false) => Value::WideInt(WideInteger::from_u128( + primitive::(array, row)?.cast_unsigned(), + )), + }) +} + +/// A decimal's digits, at whatever width DuckDB sent them. +fn decimal_mantissa(array: &dyn Array, row: usize) -> Read { + match array.data_type() { + DataType::Decimal32(..) => primitive::(array, row).map(i128::from), + DataType::Decimal64(..) => primitive::(array, row).map(i128::from), + _ => primitive::(array, row), + } +} + +/// A string column's text, whichever Arrow layout carries it. +pub(crate) fn string(array: &dyn Array, row: usize) -> Read<&str> { + if let Some(values) = array.as_string_opt::() { + return Ok(values.value(row)); + } + if let Some(values) = array.as_string_opt::() { + return Ok(values.value(row)); + } + if let Some(values) = array.as_string_view_opt() { + return Ok(values.value(row)); + } + dictionary_label(array, row) +} + +/// An ENUM's label: its dictionary entry for the row's key. +fn dictionary_label(array: &dyn Array, row: usize) -> Read<&str> { + let (key, values) = if let Some(dictionary) = array.as_dictionary_opt::() { + (usize::from(dictionary.keys().value(row)), dictionary.values()) + } else if let Some(dictionary) = array.as_dictionary_opt::() { + (usize::from(dictionary.keys().value(row)), dictionary.values()) + } else if let Some(dictionary) = array.as_dictionary_opt::() { + let key = usize::try_from(dictionary.keys().value(row)).map_err(|_| mismatch())?; + (key, dictionary.values()) + } else { + return Err(mismatch()); + }; + if key >= values.len() { + return Err(UndecodableReason::OutOfRange); + } + string(values.as_ref(), key) +} + +pub(crate) fn bytes(array: &dyn Array, row: usize) -> Read<&[u8]> { + if let Some(values) = array.as_binary_opt::() { + return Ok(values.value(row)); + } + if let Some(values) = array.as_binary_opt::() { + return Ok(values.value(row)); + } + if let Some(values) = array.as_binary_view_opt() { + return Ok(values.value(row)); + } + array + .as_fixed_size_binary_opt() + .map(|values| values.value(row)) + .ok_or(mismatch()) +} + +fn uuid(array: &dyn Array, row: usize) -> Read { + if let Some(values) = array.as_fixed_size_binary_opt() { + return uuid::Uuid::from_slice(values.value(row)).map_err(|_| mismatch()); + } + uuid::Uuid::parse_str(string(array, row)?).map_err(|_| mismatch()) +} + +/// A BIGNUM as its digits, where DuckDB sent them as text, and as an +/// integer where it fits one. +fn big_number(array: &dyn Array, row: usize) -> Read { + let text = string(array, row).map_err(|_| UndecodableReason::UnsupportedType)?; + Ok(match text.parse::() { + Ok(number) => Value::WideInt(number), + Err(_) => Value::Text(text.to_owned()), + }) +} + +pub(crate) fn date(days: i32) -> Read> { + match days { + DATE_INFINITY => Ok(Temporal::Infinity), + days if days == -DATE_INFINITY => Ok(Temporal::NegInfinity), + days => days + .checked_add(DAYS_TO_UNIX_EPOCH_FROM_CE) + .and_then(NaiveDate::from_num_days_from_ce_opt) + .map(Temporal::Finite) + .ok_or(UndecodableReason::OutOfRange), + } +} + +/// TIME and TIME_NS: DuckDB's TIME runs to 24:00:00 inclusive. +pub(crate) fn time_of_day(array: &dyn Array, row: usize) -> Read { + let nanos = match array.data_type() { + DataType::Time64(TimeUnit::Nanosecond) => i128::from(primitive::(array, row)?), + _ => i128::from(primitive::(array, row)?) * 1_000, + }; + time_from_nanos(nanos) +} + +pub(crate) fn time_from_nanos(nanos: i128) -> Read { + let seconds = nanos.div_euclid(1_000_000_000); + let fraction = u32::try_from(nanos.rem_euclid(1_000_000_000)).map_err(|_| UndecodableReason::OutOfRange)?; + let hours = u32::try_from(seconds / 3_600).map_err(|_| UndecodableReason::OutOfRange)?; + let minutes = u8::try_from(seconds / 60 % 60).map_err(|_| UndecodableReason::OutOfRange)?; + let whole = u8::try_from(seconds % 60).map_err(|_| UndecodableReason::OutOfRange)?; + SqlTime::new(false, hours, minutes, whole, fraction).map_err(|_| UndecodableReason::OutOfRange) +} + +/// A timestamp in whatever unit it counts, as the wall time it holds. +pub(crate) fn timestamp(array: &dyn Array, row: usize) -> Read> { + let (count, unit) = match array.data_type() { + DataType::Timestamp(TimeUnit::Second, _) => (primitive::(array, row)?, TimeUnit::Second), + DataType::Timestamp(TimeUnit::Millisecond, _) => ( + primitive::(array, row)?, + TimeUnit::Millisecond, + ), + DataType::Timestamp(TimeUnit::Nanosecond, _) => { + (primitive::(array, row)?, TimeUnit::Nanosecond) + } + _ => ( + primitive::(array, row)?, + TimeUnit::Microsecond, + ), + }; + match count { + TIMESTAMP_INFINITY => return Ok(Temporal::Infinity), + count if count == -TIMESTAMP_INFINITY => return Ok(Temporal::NegInfinity), + _ => {} + } + let stamp = match unit { + TimeUnit::Second => DateTime::from_timestamp(count, 0), + TimeUnit::Millisecond => DateTime::from_timestamp_millis(count), + TimeUnit::Nanosecond => Some(DateTime::from_timestamp_nanos(count)), + TimeUnit::Microsecond => DateTime::from_timestamp_micros(count), + }; + stamp + .map(|stamp| Temporal::Finite(stamp.naive_utc())) + .ok_or(UndecodableReason::OutOfRange) +} + +fn utc_offset() -> FixedOffset { + Utc.fix() +} diff --git a/linux/crates/drivers/duckdb/src/decode/nested.rs b/linux/crates/drivers/duckdb/src/decode/nested.rs new file mode 100644 index 0000000000..e4a78edaf3 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/decode/nested.rs @@ -0,0 +1,100 @@ +//! LIST, ARRAY, STRUCT, MAP and UNION values as DuckDB's own text. + +use duckdb::arrow::array::{Array, AsArray}; + +use super::text::{escaped, scalar_text}; +use crate::duck_type::DuckType; + +/// The value at `row` as DuckDB prints it, or `None` where a part of it +/// cannot be read off Arrow. +pub(super) fn text(array: &dyn Array, duck: &DuckType, row: usize) -> Option { + match duck { + DuckType::List(element) => list(array, element, row), + DuckType::Array(element, _) => { + let values = array.as_fixed_size_list_opt()?; + let width = usize::try_from(values.value_length()).ok()?; + let start = values.value_offset(row); + let start = usize::try_from(start).ok()?; + items(values.values().as_ref(), element, start..start + width) + } + DuckType::Struct(fields) => { + let values = array.as_struct_opt()?; + let mut parts = Vec::with_capacity(fields.len()); + for (index, (name, field)) in fields.iter().enumerate() { + let column = values.columns().get(index)?; + parts.push(format!( + "{}: {}", + escaped(name, true), + part(column.as_ref(), field, row)? + )); + } + Some(format!("{{{}}}", parts.join(", "))) + } + DuckType::Map(key, value) => { + let values = array.as_map_opt()?; + let offsets = values.value_offsets(); + let (start, end) = ( + usize::try_from(*offsets.get(row)?).ok()?, + usize::try_from(*offsets.get(row + 1)?).ok()?, + ); + let mut parts = Vec::with_capacity(end - start); + for entry in start..end { + parts.push(format!( + "{}={}", + part(values.keys().as_ref(), key, entry)?, + part(values.values().as_ref(), value, entry)? + )); + } + Some(format!("{{{}}}", parts.join(", "))) + } + DuckType::Union(members) => { + let values = array.as_union_opt()?; + let tag = values.type_id(row); + let (_, member) = members.get(usize::try_from(tag).ok()?)?; + let child = values.child(tag); + let at = values.value_offset(row); + match child.is_null(at) { + true => Some("NULL".to_owned()), + false => whole(child.as_ref(), member, at), + } + } + scalar => scalar_text(array, scalar, row), + } +} + +fn list(array: &dyn Array, element: &DuckType, row: usize) -> Option { + if let Some(values) = array.as_list_opt::() { + let offsets = values.value_offsets(); + let range = usize::try_from(*offsets.get(row)?).ok()?..usize::try_from(*offsets.get(row + 1)?).ok()?; + return items(values.values().as_ref(), element, range); + } + let values = array.as_list_opt::()?; + let offsets = values.value_offsets(); + let range = usize::try_from(*offsets.get(row)?).ok()?..usize::try_from(*offsets.get(row + 1)?).ok()?; + items(values.values().as_ref(), element, range) +} + +fn items(values: &dyn Array, element: &DuckType, range: std::ops::Range) -> Option { + let parts: Option> = range.map(|index| part(values, element, index)).collect(); + Some(format!("[{}]", parts?.join(", "))) +} + +/// One member of a nested value: NULL, a nested value as it prints, or +/// a scalar escaped as DuckDB escapes one there. +fn part(values: &dyn Array, duck: &DuckType, row: usize) -> Option { + if values.is_null(row) { + return Some("NULL".to_owned()); + } + match duck.is_nested() { + true => text(values, duck, row), + false => Some(escaped(&scalar_text(values, duck, row)?, false)), + } +} + +/// A union prints its member's own text, unescaped. +fn whole(values: &dyn Array, duck: &DuckType, row: usize) -> Option { + match duck.is_nested() { + true => text(values, duck, row), + false => scalar_text(values, duck, row), + } +} diff --git a/linux/crates/drivers/duckdb/src/decode/text.rs b/linux/crates/drivers/duckdb/src/decode/text.rs new file mode 100644 index 0000000000..7fd62fcfdf --- /dev/null +++ b/linux/crates/drivers/duckdb/src/decode/text.rs @@ -0,0 +1,261 @@ +//! A value inside a nested one, spelled as DuckDB's CAST to VARCHAR +//! spells it, so a list in an editor run reads as it does in a table tab. + +use chrono::NaiveDateTime; +use duckdb::arrow::array::{Array, AsArray}; +use duckdb::arrow::datatypes::{Float32Type, Float64Type, IntervalMonthDayNanoType}; +use tablepro_core::value::{SqlDecimal, Temporal}; + +use super::{bit_string, bytes, date, decimal_mantissa, integer, string, time_of_day, timestamp, uuid}; +use crate::duck_type::DuckType; + +/// A scalar's text, or `None` for one that cannot be read off Arrow. +pub(super) fn scalar_text(array: &dyn Array, duck: &DuckType, row: usize) -> Option { + Some(match duck { + DuckType::Boolean => array.as_boolean_opt()?.value(row).to_string(), + DuckType::Integer { width, signed } => { + tablepro_core::export::value_to_text(&integer(array, *width, *signed, row).ok()?)? + } + DuckType::Float => float_text(f64::from(array.as_primitive_opt::()?.value(row)), true), + DuckType::Double => float_text(array.as_primitive_opt::()?.value(row), false), + DuckType::Decimal { scale, .. } => decimal_text(decimal_mantissa(array, row).ok()?, *scale), + DuckType::Varchar | DuckType::Enum | DuckType::Json => string(array, row).ok()?.to_owned(), + DuckType::Blob => blob_text(bytes(array, row).ok()?), + DuckType::Uuid => uuid(array, row).ok()?.to_string(), + DuckType::Bit => bit_string(bytes(array, row).ok()?)?.to_string(), + DuckType::Date => match date( + array + .as_primitive_opt::()? + .value(row), + ) + .ok()? + { + Temporal::Finite(day) => crate::encode::date_text(day), + Temporal::Infinity => "infinity".to_owned(), + Temporal::NegInfinity => "-infinity".to_owned(), + }, + DuckType::Time | DuckType::TimeNs => time_of_day(array, row).ok()?.format(None), + DuckType::Timestamp(_) => stamp_text(timestamp(array, row).ok()?, ""), + // Without the ICU extension DuckDB keeps no time zone but UTC. + DuckType::TimestampTz => stamp_text(timestamp(array, row).ok()?, "+00"), + DuckType::Interval => { + let interval = array.as_primitive_opt::()?.value(row); + interval_text( + interval.months, + interval.days, + super::interval_micros(interval.nanoseconds)?, + ) + } + _ => return None, + }) +} + +/// Whether DuckDB quotes this text inside a nested value: an empty or +/// padded string, the word null, or one holding a character that would +/// read as part of the nesting. +pub(super) fn needs_quotes(text: &str, key: bool) -> bool { + let bytes = text.as_bytes(); + let space = |byte: &u8| matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'); + key || bytes.is_empty() + || bytes.first().is_some_and(space) + || (bytes.len() >= 2 && bytes.last().is_some_and(space)) + || text.eq_ignore_ascii_case("null") + || bytes.iter().any(|byte| { + matches!( + byte, + b'"' | b'\'' | b'(' | b')' | b',' | b':' | b'=' | b'[' | b']' | b'{' | b'}' + ) + }) +} + +/// A string as DuckDB writes it inside a list, a struct or a map. +pub(super) fn escaped(text: &str, key: bool) -> String { + if !needs_quotes(text, key) { + return text.to_owned(); + } + let mut quoted = String::with_capacity(text.len() + 2); + quoted.push('\''); + for character in text.chars() { + if matches!(character, '\'' | '\\') { + quoted.push('\\'); + } + quoted.push(character); + } + quoted.push('\''); + quoted +} + +/// A double as DuckDB prints one: the shortest digits that read back +/// as the same number, in plain notation from 1e-4 up to 1e16, with a +/// `.0` on a whole number, and in exponent notation outside that. +pub(super) fn float_text(value: f64, single: bool) -> String { + if value.is_nan() { + return "nan".to_owned(); + } + if value.is_infinite() { + return if value < 0.0 { "-inf" } else { "inf" }.to_owned(); + } + // A FLOAT's value came from an f32, so it narrows back exactly. + let scientific = match single { + true => format!("{:e}", value as f32), + false => format!("{value:e}"), + }; + let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0")); + let exponent: i32 = exponent.parse().unwrap_or(0); + let negative = mantissa.starts_with('-'); + let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect(); + let sign = if negative { "-" } else { "" }; + if (-4..16).contains(&exponent) { + let point = exponent + 1; + let body = if point <= 0 { + format!("0.{}{digits}", "0".repeat(point.unsigned_abs() as usize)) + } else { + let point = point.unsigned_abs() as usize; + let padded = format!("{digits:0 first.to_owned(), + false => format!("{first}.{rest}"), + }; + let exponent_sign = if exponent < 0 { '-' } else { '+' }; + format!("{sign}{mantissa}e{exponent_sign}{:02}", exponent.unsigned_abs()) +} + +fn decimal_text(mantissa: i128, scale: u8) -> String { + let text = SqlDecimal::from_scaled_i128(mantissa, u32::from(scale)).to_string(); + let present = text.split_once('.').map_or(0, |(_, fraction)| fraction.len()); + let missing = usize::from(scale).saturating_sub(present); + match (missing, present) { + (0, _) => text, + (missing, 0) => format!("{text}.{}", "0".repeat(missing)), + (missing, _) => format!("{text}{}", "0".repeat(missing)), + } +} + +/// Printable ASCII as itself, every other byte and the quote and +/// backslash characters as `\xHH`. +fn blob_text(bytes: &[u8]) -> String { + let mut text = String::with_capacity(bytes.len()); + for byte in bytes { + if (32..=126).contains(byte) && !matches!(byte, b'\\' | b'\'' | b'"') { + text.push(char::from(*byte)); + } else { + text.push_str(&format!("\\x{byte:02X}")); + } + } + text +} + +fn stamp_text(stamp: Temporal, zone: &str) -> String { + match stamp { + Temporal::Finite(stamp) => format!("{}{zone}", crate::encode::stamp_text(&stamp)), + Temporal::Infinity => "infinity".to_owned(), + Temporal::NegInfinity => "-infinity".to_owned(), + } +} + +/// DuckDB's interval text: `1 year 2 months 3 days 04:05:06.789`. +pub(crate) fn interval_text(months: i32, days: i32, micros: i64) -> String { + let mut parts = Vec::new(); + let unit = |amount: i32, name: &str| match amount { + 1 | -1 => format!("{amount} {name}"), + _ => format!("{amount} {name}s"), + }; + let (years, months) = (months / 12, months % 12); + for (amount, name) in [(years, "year"), (months, "month"), (days, "day")] { + if amount != 0 { + parts.push(unit(amount, name)); + } + } + if micros != 0 { + let sign = if micros < 0 { "-" } else { "" }; + let magnitude = micros.unsigned_abs(); + let (hours, rest) = (magnitude / 3_600_000_000, magnitude % 3_600_000_000); + let (minutes, rest) = (rest / 60_000_000, rest % 60_000_000); + let (seconds, micros) = (rest / 1_000_000, rest % 1_000_000); + let fraction = match micros { + 0 => String::new(), + micros => format!(".{}", format!("{micros:06}").trim_end_matches('0')), + }; + parts.push(format!("{sign}{hours:02}:{minutes:02}:{seconds:02}{fraction}")); + } + match parts.is_empty() { + true => "00:00:00".to_owned(), + false => parts.join(" "), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn doubles_print_as_duckdb_prints_them() { + let cases = [ + (1.0, "1.0"), + (0.1, "0.1"), + (1e300, "1e+300"), + (1e-7, "1e-07"), + (123_456_789.123, "123456789.123"), + (1e20, "1e+20"), + (1e15, "1000000000000000.0"), + (1e16, "1e+16"), + (-2.5, "-2.5"), + (0.0001, "0.0001"), + (1.5e-5, "1.5e-05"), + ]; + for (value, shown) in cases { + assert_eq!(float_text(value, false), shown, "{value}"); + } + assert_eq!(float_text(f64::from(0.1f32), true), "0.1"); + assert_eq!(float_text(f64::NAN, false), "nan"); + assert_eq!(float_text(f64::NEG_INFINITY, false), "-inf"); + } + + #[test] + fn a_string_is_quoted_only_where_duckdb_quotes_it() { + let cases = [ + ("b c", "b c"), + ("", "''"), + ("it's", "'it\\'s'"), + ("a\\b", "a\\b"), + ("a,b", "'a,b'"), + (" lead", "' lead'"), + ("trail ", "'trail '"), + ("NULL", "'NULL'"), + ("tab\tx", "tab\tx"), + ("a(b)", "'a(b)'"), + ]; + for (text, shown) in cases { + assert_eq!(escaped(text, false), shown, "{text:?}"); + } + assert_eq!(escaped("a", true), "'a'", "a struct key is always quoted"); + } + + #[test] + fn intervals_print_as_duckdb_prints_them() { + assert_eq!( + interval_text(14, 3, 14_706_789_000), + "1 year 2 months 3 days 04:05:06.789" + ); + assert_eq!(interval_text(0, -1, 0), "-1 day"); + assert_eq!(interval_text(0, 0, 1), "00:00:00.000001"); + assert_eq!(interval_text(-27, 0, 0), "-2 years -3 months"); + assert_eq!(interval_text(0, 0, 90_000_000_000), "25:00:00"); + assert_eq!(interval_text(0, 0, 0), "00:00:00"); + } + + #[test] + fn blobs_and_decimals_print_as_duckdb_prints_them() { + assert_eq!(blob_text(&[0xaa, b'a', b'\\']), "\\xAAa\\x5C"); + assert_eq!(decimal_text(150, 2), "1.50"); + assert_eq!(decimal_text(-5, 3), "-0.005"); + assert_eq!(decimal_text(12, 0), "12"); + } +} diff --git a/linux/crates/drivers/duckdb/src/dialect.rs b/linux/crates/drivers/duckdb/src/dialect.rs new file mode 100644 index 0000000000..566a07ae43 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/dialect.rs @@ -0,0 +1,449 @@ +use tablepro_core::column::{ColumnInfo, ColumnKind, ColumnType, ReadForm, SqlExpression}; +use tablepro_core::ddl::DdlDialect; +use tablepro_core::dialect::{ + BindError, BindTarget, DialectCapabilities, KeysetDirection, LiteralError, Placeholder, SqlDialect, +}; +use tablepro_core::meta::{EngineRowId, EngineRowIdPart}; +use tablepro_core::sql_syntax::{ + CredentialRules, ExpressionSyntaxError, HistoryRecording, MAX_EXPRESSION_LEN, MAX_TYPE_LEN, SqlGrammar, + TypeSyntaxError, classify_for_history, parse_expression, parse_type, +}; +use tablepro_core::statement::{BoundParam, ParamType}; +use tablepro_core::value::Value; + +use crate::duck_type::column_type_from_text; +use crate::encode::duck_text; + +/// How DuckDB spells SQL. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct DuckDbDialect; + +impl DuckDbDialect { + pub fn new() -> Self { + Self + } + + pub(crate) fn string_literal(&self, text: &str) -> String { + let quoted = |piece: &str| format!("'{}'", piece.replace('\'', "''")); + // A NUL inside quotes would end the literal where DuckDB reads + // the text as a C string, so each one is joined in with chr(0). + match text.contains('\0') { + false => quoted(text), + true => { + let pieces: Vec = text.split('\0').map(quoted).collect(); + format!("({})", pieces.join(" || chr(0) || ")) + } + } + } +} + +impl SqlDialect for DuckDbDialect { + fn capabilities(&self) -> DialectCapabilities { + DialectCapabilities { exact_row_counts: true } + } + + fn grammar(&self) -> SqlGrammar { + SqlGrammar::DuckDb + } + + fn quote_identifier(&self, name: &str) -> String { + format!("\"{}\"", name.replace('"', "\"\"")) + } + + /// A value DuckDB has no binding for goes as text, cast to the + /// column's own type, so it is parsed the way the column reads it. + fn placeholder(&self, _ordinal: usize, value: Value, target: BindTarget<'_>) -> Result { + let column = match target { + BindTarget::Pattern => return Ok(Placeholder::bound("?", BoundParam::new(value, ParamType::Text)?)), + BindTarget::EngineRowId(_) => { + return Err(BindError::Unrepresentable { + expected: "a DuckDB value", + found: "a row address", + }); + } + BindTarget::Column(column) => column, + }; + let (value, param_type) = value_param(value, &column.column_type)?; + let sql = match &column.column_type.catalog { + Some(catalog) if needs_cast(&column.column_type) => format!("CAST(? AS {})", catalog.cast_target.as_sql()), + _ => "?".to_owned(), + }; + Ok(Placeholder::bound(sql, BoundParam::new(value, param_type)?)) + } + + fn literal(&self, value: &Value, column: &ColumnType) -> Result { + match value { + Value::Null => Ok("NULL".to_owned()), + Value::Bool(flag) => Ok(if *flag { "TRUE" } else { "FALSE" }.to_owned()), + Value::Int(integer) => Ok(integer.to_string()), + Value::UInt(integer) => Ok(integer.to_string()), + Value::WideInt(integer) => Ok(integer.to_string()), + Value::Decimal(decimal) if decimal.is_finite() => Ok(decimal.to_string()), + // Debug keeps the point or the exponent, so `2.0` stays a + // DOUBLE instead of reading back as the integer 2. + Value::Float32(real) if real.is_finite() => Ok(format!("CAST({:?} AS FLOAT)", f64::from(*real))), + Value::Float64(real) if real.is_finite() => Ok(format!("CAST({real:?} AS DOUBLE)")), + Value::Float32(real) => Ok(format!("CAST('{}' AS FLOAT)", non_finite(f64::from(*real)))), + Value::Float64(real) => Ok(format!("CAST('{}' AS DOUBLE)", non_finite(*real))), + Value::Text(text) => Ok(self.string_literal(text)), + Value::Bytes(bytes) => { + let escaped: String = bytes.iter().map(|byte| format!("\\x{byte:02X}")).collect(); + Ok(format!("CAST('{escaped}' AS BLOB)")) + } + Value::Undecodable(undecodable) => Err(LiteralError::Undecodable { + reason: format!("{:?}", undecodable.reason), + }), + Value::Array(_) | Value::Decimal(_) => Err(LiteralError::Unrepresentable { + found: value.variant_name(), + }), + // DuckDB writes a UNION as its member's value alone, and reads + // that text back as the first member it fits, which may be + // another one. + Value::Other(other) if names_union(&other.type_name) || names_union(&column.name) => { + Err(LiteralError::Unrepresentable { + found: "a UNION value", + }) + } + other => { + let text = duck_text(other).ok_or(LiteralError::Unrepresentable { + found: other.variant_name(), + })?; + let target = column + .catalog + .as_ref() + .map(|catalog| catalog.cast_target.as_sql().to_owned()) + .or_else(|| type_of(other).map(str::to_owned)); + Ok(match target { + Some(target) => format!("CAST({} AS {target})", self.string_literal(&text)), + None => self.string_literal(&text), + }) + } + } + } + + fn escape_like_text(&self, text: &str) -> String { + let mut escaped = String::with_capacity(text.len()); + for character in text.chars() { + if matches!(character, '!' | '%' | '_') { + escaped.push('!'); + } + escaped.push(character); + } + escaped + } + + fn row_window(&self, order_by: Option<&str>, limit: u64, offset: u64) -> String { + match order_by.map(str::trim).filter(|order| !order.is_empty()) { + Some(order) => format!(" ORDER BY {order} LIMIT {limit} OFFSET {offset}"), + None => format!(" LIMIT {limit} OFFSET {offset}"), + } + } + + fn row_value_after(&self, key_sql: &[String], markers: &[String], direction: KeysetDirection) -> String { + format!( + "({}) {} ({})", + key_sql.join(", "), + direction.comparison(), + markers.join(", ") + ) + } + + /// DuckDB's rowid moves when a checkpoint compacts a table, so it + /// names no row. + fn engine_row_id(&self, _id: EngineRowId) -> Option { + None + } + + fn engine_row_id_column(&self, _part: EngineRowIdPart) -> Option { + None + } + + fn parse_type(&self, text: &str) -> Result { + let parsed = parse_type(SqlGrammar::DuckDb, text, MAX_TYPE_LEN)?; + Ok(column_type_from_text(&parsed.text)) + } + + fn parse_expression(&self, text: &str) -> Result { + let parsed = parse_expression(SqlGrammar::DuckDb, text, MAX_EXPRESSION_LEN)?; + Ok(SqlExpression::from_catalog_text(parsed.text)) + } + + fn history_recording(&self, sql: &str) -> HistoryRecording { + classify_for_history(SqlGrammar::DuckDb, &CREDENTIAL_RULES, sql) + } + + fn ddl(&self) -> Option<&dyn DdlDialect> { + Some(self) + } + + /// A value Arrow cannot carry whole is read as DuckDB's own text. + fn projection(&self, column: &ColumnInfo) -> String { + let name = self.quote_identifier(&column.name); + match column.column_type.read_form { + ReadForm::ServerText | ReadForm::ServerNumeric => format!("CAST({name} AS VARCHAR) AS {name}"), + ReadForm::Native => name, + } + } + + /// DuckDB casts nothing to text on its own for LIKE. + fn text_operand(&self, column: &ColumnInfo) -> Option { + match column.column_type.kind { + ColumnKind::Text { .. } | ColumnKind::Json { .. } => None, + _ => Some(format!("CAST({} AS VARCHAR)", self.quote_identifier(&column.name))), + } + } + + /// DuckDB's indexes do not order a scan, so ORDER BY the key sorts + /// every row in memory, while `preserve_insertion_order`, on by + /// default, returns a plain scan in storage order. + fn key_order_sorts_the_table(&self) -> bool { + true + } +} + +/// Whether a value for the column goes as text DuckDB has to cast: a +/// column whose type has no binding of its own. +fn needs_cast(column: &ColumnType) -> bool { + !matches!( + column.kind, + ColumnKind::Bool(_) + | ColumnKind::Integer(_) + | ColumnKind::Float(_) + | ColumnKind::Text { .. } + | ColumnKind::Bytes { .. } + | ColumnKind::Date + | ColumnKind::Interval + | ColumnKind::Enum { .. } + ) +} + +/// The parameter a value takes for a column. +fn value_param(value: Value, column: &ColumnType) -> Result<(Value, ParamType), BindError> { + if let Value::Other(other) = value { + let target = column.catalog.as_ref().map_or_else( + || tablepro_core::column::SqlTypeExpr::from_catalog_text(column.name.clone()), + |catalog| catalog.cast_target.clone(), + ); + return Ok((Value::Text(other.text), ParamType::ServerText(target))); + } + let param_type = match &value { + Value::Null | Value::Text(_) => ParamType::Text, + Value::Bool(_) => ParamType::Bool, + Value::Int(_) => ParamType::Int64, + Value::UInt(_) => ParamType::UInt64, + Value::WideInt(_) | Value::Decimal(_) => ParamType::Decimal, + Value::Float32(_) => ParamType::Float32, + Value::Float64(_) => ParamType::Float64, + Value::Bytes(_) => ParamType::Bytes, + Value::Bits(_) => ParamType::Bits, + Value::Uuid(_) => ParamType::Uuid, + Value::Json(_) => ParamType::Json, + Value::Date(_) => ParamType::Date, + Value::Time(_) => ParamType::Time, + Value::TimeTz(_) => ParamType::TimeTz, + Value::Timestamp(_) => ParamType::Timestamp, + Value::TimestampTz(_) => ParamType::TimestampTz, + Value::Interval(_) => ParamType::Interval, + Value::Other(_) | Value::Undecodable(_) => ParamType::Text, + Value::Array(_) => { + return Err(BindError::Unrepresentable { + expected: "a DuckDB value", + found: "Array", + }); + } + }; + Ok((value, param_type)) +} + +/// Whether a DuckDB type name holds a UNION, at any depth. +fn names_union(type_name: &str) -> bool { + type_name.to_ascii_uppercase().contains("UNION(") +} + +/// The type a literal of `value` is cast to when the column names none. +fn type_of(value: &Value) -> Option<&'static str> { + Some(match value { + Value::Uuid(_) => "UUID", + Value::Json(_) => "JSON", + Value::Date(_) => "DATE", + Value::Time(_) => "TIME", + Value::TimeTz(_) => "TIMETZ", + Value::Timestamp(_) => "TIMESTAMP", + Value::TimestampTz(_) => "TIMESTAMPTZ", + Value::Interval(_) => "INTERVAL", + Value::Bits(_) => "BIT", + _ => return None, + }) +} + +fn non_finite(real: f64) -> &'static str { + match (real.is_nan(), real.is_sign_negative()) { + (true, _) => "nan", + (false, true) => "-inf", + (false, false) => "inf", + } +} + +/// The settings that hold a credential, and PRAGMA's Parquet key. +const SECRET_SETTINGS: &[&str] = &[ + "s3_access_key_id", + "s3_secret_access_key", + "s3_session_token", + "azure_storage_connection_string", + "azure_account_key", + "azure_proxy_password", + "motherduck_token", + "http_proxy", + "http_proxy_password", + "hf_token", + "add_parquet_key", +]; + +/// Statements and calls whose text can hold a secret: a stored secret, +/// a setting that is one, and the connection strings of the scanners +/// that reach other databases. ATTACH names any database, an encrypted +/// file's key or a server's login among them. +const CREDENTIAL_RULES: CredentialRules = CredentialRules { + statement_prefixes: &[ + &["CREATE", "SECRET"], + &["CREATE", "PERSISTENT", "SECRET"], + &["CREATE", "TEMPORARY", "SECRET"], + &["CREATE", "TEMP", "SECRET"], + &["ATTACH"], + ], + // A setting is set by SET or by PRAGMA, and PRAGMA also adds a + // Parquet key. + conditional_prefixes: &[(&["SET"], SECRET_SETTINGS), (&["PRAGMA"], SECRET_SETTINGS)], + carriers: &[ + "postgres_attach", + "postgres_scan", + "postgres_scan_pushdown", + "postgres_query", + "postgres_execute", + "mysql_query", + "mysql_execute", + "add_parquet_key", + ], +}; + +#[cfg(test)] +mod tests { + use tablepro_core::column::{CatalogType, ColumnDefault, SqlTypeExpr, TextLength}; + use tablepro_core::sql_syntax::WithholdReason; + + use super::*; + + fn column(declaration: &str) -> ColumnInfo { + ColumnInfo { + name: "v".to_owned(), + column_type: column_type_from_text(declaration), + nullable: true, + primary_key: false, + is_auto_increment: false, + is_generated: false, + default: ColumnDefault::None, + comment: None, + } + } + + #[test] + fn a_value_with_no_binding_is_cast_to_its_column() { + let dialect = DuckDbDialect; + let decimal = column("DECIMAL(38,10)"); + let integer = column("INTEGER"); + + let bound = dialect + .placeholder(0, Value::Decimal("1.5".parse().unwrap()), BindTarget::Column(&decimal)) + .unwrap(); + let plain = dialect + .placeholder(0, Value::Int(1), BindTarget::Column(&integer)) + .unwrap(); + + assert_eq!(bound.sql, "CAST(? AS DECIMAL(38,10))"); + assert_eq!(plain.sql, "?"); + } + + #[test] + fn a_union_value_has_no_literal() { + let dialect = DuckDbDialect; + let union = column("UNION(n INTEGER, s VARCHAR)"); + let list = column("UNION(n INTEGER, s VARCHAR)[]"); + let value = |type_name: &str| { + Value::Other(Box::new(tablepro_core::value::OtherValue { + type_name: type_name.to_owned(), + text: "2".to_owned(), + })) + }; + + assert!(dialect.literal(&value("UNION(n INTEGER, s VARCHAR)"), &union.column_type).is_err()); + assert!(dialect.literal(&value("UNION(n INTEGER, s VARCHAR)[]"), &list.column_type).is_err()); + assert!(dialect.literal(&value("INTEGER[]"), &column("INTEGER[]").column_type).is_ok()); + } + + #[test] + fn literals_are_spelled_as_duckdb_reads_them() { + let dialect = DuckDbDialect; + let text = ColumnType { + name: "VARCHAR".to_owned(), + kind: ColumnKind::Text { + length: TextLength::Unbounded, + }, + catalog: Some(CatalogType { + declaration: SqlTypeExpr::from_catalog_text("VARCHAR"), + cast_target: SqlTypeExpr::from_catalog_text("VARCHAR"), + }), + dynamic_storage: false, + read_form: ReadForm::Native, + }; + + assert_eq!( + dialect.literal(&Value::Text("it's".to_owned()), &text).unwrap(), + "'it''s'" + ); + assert_eq!( + dialect.literal(&Value::Text("a\0b".to_owned()), &text).unwrap(), + "('a' || chr(0) || 'b')" + ); + assert_eq!( + dialect.literal(&Value::Bytes(vec![0xde, 0x01]), &text).unwrap(), + "CAST('\\xDE\\x01' AS BLOB)" + ); + assert_eq!( + dialect.literal(&Value::Float64(f64::NAN), &text).unwrap(), + "CAST('nan' AS DOUBLE)" + ); + assert_eq!(dialect.literal(&Value::Bool(true), &text).unwrap(), "TRUE"); + } + + #[test] + fn a_nested_column_reads_as_duckdb_text() { + assert_eq!( + DuckDbDialect.projection(&column("INTEGER[]")), + "CAST(\"v\" AS VARCHAR) AS \"v\"" + ); + assert_eq!(DuckDbDialect.projection(&column("INTEGER")), "\"v\""); + } + + #[test] + fn secrets_and_connection_strings_stay_out_of_the_history() { + let withheld = HistoryRecording::Withhold(WithholdReason::CredentialStatement); + for sql in [ + "CREATE SECRET s (TYPE s3, KEY_ID 'k', SECRET 'v')", + "CREATE OR REPLACE PERSISTENT SECRET s (TYPE s3, KEY_ID 'k', SECRET 'v')", + "ATTACH 'host=db user=u password=p' AS pg (TYPE postgres)", + "SET s3_secret_access_key = 'v'", + "SET GLOBAL motherduck_token = 'v'", + "SELECT * FROM postgres_scan('host=db password=p', 'public', 't')", + "PRAGMA add_parquet_key('k', '0123456789112345')", + "PRAGMA http_proxy_password = 'v'", + "PRAGMA s3_secret_access_key='v'", + "SET http_proxy = 'http://u:p@proxy:3128'", + ] { + assert_eq!(DuckDbDialect.history_recording(sql), withheld, "{sql}"); + } + assert_eq!( + tablepro_test_support::history_cases::unrecorded_plain_statements(&DuckDbDialect), + Vec::<&str>::new() + ); + } +} diff --git a/linux/crates/drivers/duckdb/src/driver.rs b/linux/crates/drivers/duckdb/src/driver.rs new file mode 100644 index 0000000000..a94949bbd7 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/driver.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use tablepro_core::{ + ConnectOptions, ConnectTarget, Connection, DatabaseDriver, DriverCapabilities, DriverError, TimeoutPhase, + UnsupportedFeature, +}; +use tokio_util::sync::CancellationToken; + +use crate::capabilities::CAPABILITIES; +use crate::connection::DuckDbConnection; + +/// The DuckDB engine, compiled into the app from the sources the duckdb +/// crate ships. +#[derive(Debug, Clone, Copy, Default)] +pub struct DuckDbDriver; + +#[async_trait] +impl DatabaseDriver for DuckDbDriver { + fn id(&self) -> &'static str { + "duckdb" + } + + fn display_name(&self) -> &'static str { + "DuckDB" + } + + fn capabilities(&self) -> &'static DriverCapabilities { + &CAPABILITIES + } + + async fn connect( + &self, + options: ConnectOptions, + cancel: CancellationToken, + ) -> Result, DriverError> { + let ConnectTarget::File(endpoint) = &options.target else { + return Err(DriverError::Unsupported { + feature: UnsupportedFeature::NetworkConnection, + }); + }; + let opening = DuckDbConnection::open(endpoint, options.workspace_settings, options.liveness); + tokio::select! { + biased; + () = cancel.cancelled() => Err(DriverError::Cancelled), + opened = tokio::time::timeout(options.liveness.connect_timeout, opening) => match opened { + Ok(opened) => opened.map(|connection| Arc::new(connection) as Arc), + Err(_) => Err(DriverError::Timeout { + phase: TimeoutPhase::Connect, + server_cancelled: false, + }), + }, + } + } +} diff --git a/linux/crates/drivers/duckdb/src/duck_type.rs b/linux/crates/drivers/duckdb/src/duck_type.rs new file mode 100644 index 0000000000..ce1aa3f2cf --- /dev/null +++ b/linux/crates/drivers/duckdb/src/duck_type.rs @@ -0,0 +1,555 @@ +//! DuckDB's types: what a result column is, how it is spelled, and what +//! the grid makes of it. + +use duckdb::core::{LogicalTypeHandle, LogicalTypeId}; +use tablepro_core::column::{ + BoolStorage, CatalogType, ColumnKind, ColumnType, FloatWidth, IntegerKind, IntegerWidth, ReadForm, SqlTypeExpr, + TemporalZone, TextLength, TimeRange, +}; + +/// The unit a timestamp counts in. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Stamp { + Seconds, + Millis, + Micros, + Nanos, +} + +/// A column's DuckDB type, with the children a nested one holds, as +/// far as a value has to be read by it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum DuckType { + Boolean, + Integer { + width: IntegerWidth, + signed: bool, + }, + Float, + Double, + Decimal { + width: u8, + scale: u8, + }, + Varchar, + Json, + Blob, + Uuid, + Bit, + BigNum, + Enum, + Date, + Time, + TimeNs, + TimeTz, + Timestamp(Stamp), + TimestampTz, + Interval, + List(Box), + Array(Box, u64), + Struct(Vec<(String, DuckType)>), + Map(Box, Box), + Union(Vec<(String, DuckType)>), + Geometry, + Variant, + Null, + /// A type this driver does not read, by DuckDB's name or id. + Other(String), +} + +impl DuckType { + /// The type of a result column, as DuckDB reported it. + pub(crate) fn from_logical(handle: &LogicalTypeHandle) -> Self { + let id = match handle.try_id() { + Ok(id) => id, + Err(raw) => return Self::Other(format!("type {raw}")), + }; + if handle + .get_alias() + .is_some_and(|alias| alias.eq_ignore_ascii_case("JSON")) + { + return Self::Json; + } + Self::from_id(id, handle).unwrap_or_else(|| Self::Other(format!("{id:?}").to_ascii_uppercase())) + } + + fn from_id(id: LogicalTypeId, handle: &LogicalTypeHandle) -> Option { + let integer = |width, signed| Self::Integer { width, signed }; + Some(match id { + LogicalTypeId::Boolean => Self::Boolean, + LogicalTypeId::Tinyint => integer(IntegerWidth::W8, true), + LogicalTypeId::Smallint => integer(IntegerWidth::W16, true), + LogicalTypeId::Integer => integer(IntegerWidth::W32, true), + LogicalTypeId::Bigint => integer(IntegerWidth::W64, true), + LogicalTypeId::Hugeint => integer(IntegerWidth::W128, true), + LogicalTypeId::UTinyint => integer(IntegerWidth::W8, false), + LogicalTypeId::USmallint => integer(IntegerWidth::W16, false), + LogicalTypeId::UInteger => integer(IntegerWidth::W32, false), + LogicalTypeId::UBigint => integer(IntegerWidth::W64, false), + LogicalTypeId::UHugeint => integer(IntegerWidth::W128, false), + LogicalTypeId::Float => Self::Float, + LogicalTypeId::Double => Self::Double, + LogicalTypeId::Decimal => Self::Decimal { + width: handle.decimal_width(), + scale: handle.decimal_scale(), + }, + LogicalTypeId::Varchar => Self::Varchar, + LogicalTypeId::Blob => Self::Blob, + LogicalTypeId::Uuid => Self::Uuid, + LogicalTypeId::Bit => Self::Bit, + LogicalTypeId::Bignum => Self::BigNum, + LogicalTypeId::Enum => Self::Enum, + LogicalTypeId::Date => Self::Date, + LogicalTypeId::Time => Self::Time, + LogicalTypeId::TimeNs => Self::TimeNs, + LogicalTypeId::TimeTZ => Self::TimeTz, + LogicalTypeId::Timestamp => Self::Timestamp(Stamp::Micros), + LogicalTypeId::TimestampS => Self::Timestamp(Stamp::Seconds), + LogicalTypeId::TimestampMs => Self::Timestamp(Stamp::Millis), + LogicalTypeId::TimestampNs => Self::Timestamp(Stamp::Nanos), + LogicalTypeId::TimestampTZ => Self::TimestampTz, + LogicalTypeId::Interval => Self::Interval, + LogicalTypeId::Geometry => Self::Geometry, + LogicalTypeId::Variant => Self::Variant, + LogicalTypeId::SqlNull => Self::Null, + LogicalTypeId::List => Self::List(Box::new(child(handle, 0)?)), + LogicalTypeId::Array => Self::Array(Box::new(child(handle, 0)?), 0), + LogicalTypeId::Map => Self::Map(Box::new(child(handle, 0)?), Box::new(child(handle, 1)?)), + LogicalTypeId::Struct => Self::Struct(members(handle)?), + LogicalTypeId::Union => Self::Union(members(handle)?), + _ => return None, + }) + } + + pub(crate) fn is_nested(&self) -> bool { + matches!( + self, + Self::List(_) | Self::Array(..) | Self::Struct(_) | Self::Map(..) | Self::Union(_) + ) + } + + /// DuckDB's own spelling, as its catalogue writes it. + pub(crate) fn name(&self) -> String { + match self { + Self::Boolean => "BOOLEAN".to_owned(), + Self::Integer { width, signed } => integer_name(*width, *signed).to_owned(), + Self::Float => "FLOAT".to_owned(), + Self::Double => "DOUBLE".to_owned(), + Self::Decimal { width, scale } => format!("DECIMAL({width},{scale})"), + Self::Varchar => "VARCHAR".to_owned(), + Self::Json => "JSON".to_owned(), + Self::Blob => "BLOB".to_owned(), + Self::Uuid => "UUID".to_owned(), + Self::Bit => "BIT".to_owned(), + Self::BigNum => "BIGNUM".to_owned(), + Self::Enum => "ENUM".to_owned(), + Self::Date => "DATE".to_owned(), + Self::Time => "TIME".to_owned(), + Self::TimeNs => "TIME_NS".to_owned(), + Self::TimeTz => "TIME WITH TIME ZONE".to_owned(), + Self::Timestamp(Stamp::Seconds) => "TIMESTAMP_S".to_owned(), + Self::Timestamp(Stamp::Millis) => "TIMESTAMP_MS".to_owned(), + Self::Timestamp(Stamp::Micros) => "TIMESTAMP".to_owned(), + Self::Timestamp(Stamp::Nanos) => "TIMESTAMP_NS".to_owned(), + Self::TimestampTz => "TIMESTAMP WITH TIME ZONE".to_owned(), + Self::Interval => "INTERVAL".to_owned(), + Self::List(element) => format!("{}[]", element.name()), + Self::Array(element, 0) => format!("{}[]", element.name()), + Self::Array(element, size) => format!("{}[{size}]", element.name()), + Self::Struct(fields) => format!("STRUCT({})", member_list(fields)), + Self::Map(key, value) => format!("MAP({}, {})", key.name(), value.name()), + Self::Union(members) => format!("UNION({})", member_list(members)), + Self::Geometry => "GEOMETRY".to_owned(), + Self::Variant => "VARIANT".to_owned(), + Self::Null => "NULL".to_owned(), + Self::Other(name) => name.clone(), + } + } + + /// The type of a result column, as the grid reads it. + pub(crate) fn column_type(&self) -> ColumnType { + let name = self.name(); + let catalog = + (!self.is_nested() && !matches!(self, Self::Enum | Self::Null | Self::Other(_))).then(|| CatalogType { + declaration: SqlTypeExpr::from_catalog_text(name.clone()), + cast_target: SqlTypeExpr::from_catalog_text(name.clone()), + }); + ColumnType { + kind: self.kind(Vec::new()), + read_form: self.read_form(), + name, + catalog, + dynamic_storage: false, + } + } + + fn kind(&self, labels: Vec) -> ColumnKind { + match self { + Self::Boolean => ColumnKind::Bool(BoolStorage::Native), + Self::Integer { width, signed } => ColumnKind::Integer(IntegerKind { + width: *width, + signed: *signed, + }), + Self::Float => ColumnKind::Float(FloatWidth::F32), + Self::Double => ColumnKind::Float(FloatWidth::F64), + Self::Decimal { width, scale } => ColumnKind::Decimal { + precision: Some(u32::from(*width)), + scale: Some(u32::from(*scale)), + }, + Self::Varchar => ColumnKind::Text { + length: TextLength::Unbounded, + }, + Self::Json => ColumnKind::Json { binary: false }, + Self::Blob => ColumnKind::Bytes { + length: TextLength::Unbounded, + }, + Self::Uuid => ColumnKind::Uuid, + Self::Bit => ColumnKind::BitString { + length: None, + varying: true, + }, + Self::Enum => ColumnKind::Enum { + labels, + multiple: false, + }, + Self::Date => ColumnKind::Date, + Self::Time | Self::TimeNs => time_kind(false), + Self::TimeTz => time_kind(true), + Self::Timestamp(_) => ColumnKind::Timestamp { + fractional_digits: None, + zone: TemporalZone::Naive, + }, + Self::TimestampTz => ColumnKind::Timestamp { + fractional_digits: None, + zone: TemporalZone::Offset, + }, + Self::Interval => ColumnKind::Interval, + Self::List(element) | Self::Array(element, _) => ColumnKind::Array { + element: Box::new(element.kind(Vec::new())), + }, + Self::Struct(_) | Self::Map(..) | Self::Union(_) => ColumnKind::Composite, + Self::Geometry => ColumnKind::Spatial, + Self::Variant => ColumnKind::Variant, + Self::BigNum | Self::Null | Self::Other(_) => ColumnKind::Other, + } + } + + /// How a page reads a column of this type: natively, or as DuckDB's + /// own text where the value cannot be read whole off a result. + fn read_form(&self) -> ReadForm { + match self { + Self::TimeTz + | Self::BigNum + | Self::Geometry + | Self::Variant + | Self::Other(_) + | Self::List(_) + | Self::Array(..) + | Self::Struct(_) + | Self::Map(..) + | Self::Union(_) => ReadForm::ServerText, + _ => ReadForm::Native, + } + } +} + +fn child(handle: &LogicalTypeHandle, index: usize) -> Option { + (index < handle.num_children()).then(|| DuckType::from_logical(&handle.child(index))) +} + +fn members(handle: &LogicalTypeHandle) -> Option> { + (0..handle.num_children()) + .map(|index| { + let name = handle.try_child_name(index).ok()?; + Some((name, DuckType::from_logical(&handle.child(index)))) + }) + .collect() +} + +fn member_list(members: &[(String, DuckType)]) -> String { + members + .iter() + .map(|(name, member)| format!("{} {}", quote_member(name), member.name())) + .collect::>() + .join(", ") +} + +/// A member name as DuckDB's catalogue writes it: bare when it is a +/// plain lower-case word, quoted otherwise. +fn quote_member(name: &str) -> String { + let plain = name + .chars() + .next() + .is_some_and(|first| first.is_ascii_lowercase() || first == '_') + && name + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'); + match plain { + true => name.to_owned(), + false => format!("\"{}\"", name.replace('"', "\"\"")), + } +} + +fn integer_name(width: IntegerWidth, signed: bool) -> &'static str { + match (width, signed) { + (IntegerWidth::W8, true) => "TINYINT", + (IntegerWidth::W16, true) => "SMALLINT", + (IntegerWidth::W24 | IntegerWidth::W32, true) => "INTEGER", + (IntegerWidth::W64, true) => "BIGINT", + (IntegerWidth::W128 | IntegerWidth::W256, true) => "HUGEINT", + (IntegerWidth::W8, false) => "UTINYINT", + (IntegerWidth::W16, false) => "USMALLINT", + (IntegerWidth::W24 | IntegerWidth::W32, false) => "UINTEGER", + (IntegerWidth::W64, false) => "UBIGINT", + (IntegerWidth::W128 | IntegerWidth::W256, false) => "UHUGEINT", + } +} + +fn time_kind(with_offset: bool) -> ColumnKind { + ColumnKind::Time { + fractional_digits: None, + with_offset, + range: TimeRange::TimeOfDay, + } +} + +/// A column's type from DuckDB's spelling of it, as `duckdb_columns()` +/// writes it or a user types it in the structure tab. Nested types are +/// told apart by their shape only, since the grid reads them as text. +pub(crate) fn column_type_from_text(text: &str) -> ColumnType { + let declared = text.trim(); + let duck = duck_type_from_text(declared); + let labels = match &duck { + DuckType::Enum => enum_labels(declared).unwrap_or_default(), + _ => Vec::new(), + }; + let known = !matches!(duck, DuckType::Other(_)); + ColumnType { + name: declared.to_owned(), + kind: duck.kind(labels), + catalog: Some(CatalogType { + declaration: SqlTypeExpr::from_catalog_text(declared), + cast_target: SqlTypeExpr::from_catalog_text(declared), + }), + dynamic_storage: !known, + read_form: duck.read_form(), + } +} + +/// The shape of a spelled type: enough to choose how it is read. +pub(crate) fn duck_type_from_text(text: &str) -> DuckType { + let upper = text.trim().to_ascii_uppercase(); + if let Some(element) = array_element(&upper) { + return DuckType::List(Box::new(duck_type_from_text(element))); + } + let (head, arguments) = match upper.split_once('(') { + Some((head, rest)) => (head.trim_end(), rest.strip_suffix(')')), + None => (upper.as_str(), None), + }; + match head { + "STRUCT" | "ROW" => return DuckType::Struct(Vec::new()), + "MAP" => return DuckType::Map(Box::new(DuckType::Varchar), Box::new(DuckType::Varchar)), + "UNION" => return DuckType::Union(Vec::new()), + "ENUM" => return DuckType::Enum, + "DECIMAL" | "NUMERIC" | "DEC" => return decimal(arguments), + _ => {} + } + scalar(head).unwrap_or_else(|| DuckType::Other(text.trim().to_owned())) +} + +/// `INTEGER[]` and `INTEGER[3]` hold `INTEGER`. +fn array_element(upper: &str) -> Option<&str> { + let body = upper.strip_suffix(']')?; + let open = body.rfind('[')?; + let size = &body[open + 1..]; + size.bytes().all(|b| b.is_ascii_digit()).then(|| &body[..open]) +} + +/// DuckDB's default for a bare DECIMAL is (18,3). +fn decimal(arguments: Option<&str>) -> DuckType { + let mut numbers = arguments + .unwrap_or_default() + .split(',') + .map(|part| part.trim().parse::().ok()); + match (numbers.next().flatten(), numbers.next().flatten()) { + (Some(width), Some(scale)) => DuckType::Decimal { width, scale }, + (Some(width), None) => DuckType::Decimal { width, scale: 0 }, + _ => DuckType::Decimal { width: 18, scale: 3 }, + } +} + +fn scalar(head: &str) -> Option { + let integer = |width, signed| DuckType::Integer { width, signed }; + let collapsed = head.split_ascii_whitespace().collect::>().join(" "); + Some(match collapsed.as_str() { + "BOOLEAN" | "BOOL" | "LOGICAL" => DuckType::Boolean, + "TINYINT" | "INT1" => integer(IntegerWidth::W8, true), + "SMALLINT" | "INT2" | "SHORT" | "INT16" => integer(IntegerWidth::W16, true), + "INTEGER" | "INT" | "INT4" | "SIGNED" | "INT32" => integer(IntegerWidth::W32, true), + "BIGINT" | "INT8" | "LONG" | "INT64" => integer(IntegerWidth::W64, true), + "HUGEINT" | "INT128" => integer(IntegerWidth::W128, true), + "UTINYINT" | "UINT8" => integer(IntegerWidth::W8, false), + "USMALLINT" | "UINT16" => integer(IntegerWidth::W16, false), + "UINTEGER" | "UINT32" => integer(IntegerWidth::W32, false), + "UBIGINT" | "UINT64" => integer(IntegerWidth::W64, false), + "UHUGEINT" | "UINT128" => integer(IntegerWidth::W128, false), + "FLOAT" | "FLOAT4" | "REAL" => DuckType::Float, + "DOUBLE" | "FLOAT8" | "DOUBLE PRECISION" => DuckType::Double, + "VARCHAR" | "TEXT" | "STRING" | "CHAR" | "BPCHAR" | "NVARCHAR" | "CHARACTER VARYING" | "CHARACTER" => { + DuckType::Varchar + } + "JSON" => DuckType::Json, + "BLOB" | "BYTEA" | "BINARY" | "VARBINARY" => DuckType::Blob, + "UUID" | "GUID" => DuckType::Uuid, + "BIT" | "BITSTRING" => DuckType::Bit, + "BIGNUM" | "VARINT" => DuckType::BigNum, + "DATE" => DuckType::Date, + "TIME" | "TIME WITHOUT TIME ZONE" => DuckType::Time, + "TIME_NS" => DuckType::TimeNs, + "TIMETZ" | "TIME WITH TIME ZONE" => DuckType::TimeTz, + "TIMESTAMP" | "DATETIME" | "TIMESTAMP WITHOUT TIME ZONE" | "TIMESTAMP_US" => DuckType::Timestamp(Stamp::Micros), + "TIMESTAMP_S" => DuckType::Timestamp(Stamp::Seconds), + "TIMESTAMP_MS" => DuckType::Timestamp(Stamp::Millis), + "TIMESTAMP_NS" => DuckType::Timestamp(Stamp::Nanos), + "TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => DuckType::TimestampTz, + "INTERVAL" => DuckType::Interval, + "GEOMETRY" => DuckType::Geometry, + "VARIANT" => DuckType::Variant, + "NULL" => DuckType::Null, + _ => return None, + }) +} + +/// The labels of `ENUM('a', 'it''s')`. +fn enum_labels(text: &str) -> Option> { + let open = text.find('(')?; + let body = text.get(open + 1..)?.trim_end().strip_suffix(')')?; + let mut labels = Vec::new(); + let mut chars = body.chars().peekable(); + loop { + while chars.peek().is_some_and(|c| c.is_whitespace() || *c == ',') { + chars.next(); + } + match chars.next() { + None => return Some(labels), + Some('\'') => {} + Some(_) => return None, + } + let mut label = String::new(); + loop { + match chars.next()? { + '\'' if chars.peek() == Some(&'\'') => { + chars.next(); + label.push('\''); + } + '\'' => break, + other => label.push(other), + } + } + labels.push(label); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_catalogue_spelling_reads_as_its_kind() { + let kind = |text: &str| column_type_from_text(text).kind; + + assert_eq!( + kind("DECIMAL(18,3)"), + ColumnKind::Decimal { + precision: Some(18), + scale: Some(3) + } + ); + assert_eq!( + kind("UHUGEINT"), + ColumnKind::Integer(IntegerKind { + width: IntegerWidth::W128, + signed: false + }) + ); + assert_eq!( + kind("INTEGER[]"), + ColumnKind::Array { + element: Box::new(ColumnKind::Integer(IntegerKind { + width: IntegerWidth::W32, + signed: true + })) + } + ); + assert_eq!(kind("STRUCT(a INTEGER, \"b c\" VARCHAR)"), ColumnKind::Composite); + assert_eq!(kind("MAP(VARCHAR, INTEGER)"), ColumnKind::Composite); + assert_eq!(kind("JSON"), ColumnKind::Json { binary: false }); + assert_eq!( + kind("TIMESTAMP WITH TIME ZONE"), + ColumnKind::Timestamp { + fractional_digits: None, + zone: TemporalZone::Offset + } + ); + assert_eq!(kind("geography"), ColumnKind::Other); + } + + #[test] + fn a_type_read_as_text_says_so() { + for text in [ + "TIME WITH TIME ZONE", + "BIGNUM", + "INTEGER[3]", + "MAP(VARCHAR, INTEGER)", + "GEOMETRY", + "custom", + ] { + assert_eq!(column_type_from_text(text).read_form, ReadForm::ServerText, "{text}"); + } + for text in ["INTEGER", "UUID", "BIT", "INTERVAL", "TIME_NS", "ENUM('a')"] { + assert_eq!(column_type_from_text(text).read_form, ReadForm::Native, "{text}"); + } + } + + #[test] + fn enum_labels_keep_their_quotes_and_order() { + assert_eq!( + column_type_from_text("ENUM('happy', 'it''s', '')").kind, + ColumnKind::Enum { + labels: vec!["happy".to_owned(), "it's".to_owned(), String::new()], + multiple: false + } + ); + } + + #[test] + fn aliases_a_user_types_read_as_the_type() { + assert_eq!(duck_type_from_text("int8"), duck_type_from_text("BIGINT")); + assert_eq!(duck_type_from_text("double precision"), DuckType::Double); + assert_eq!(duck_type_from_text("text"), DuckType::Varchar); + assert_eq!( + duck_type_from_text("numeric"), + DuckType::Decimal { width: 18, scale: 3 } + ); + assert_eq!(duck_type_from_text("varchar(20)"), DuckType::Varchar); + assert_eq!(duck_type_from_text("timestamptz"), DuckType::TimestampTz); + } + + #[test] + fn a_nested_name_is_spelled_as_the_catalogue_does() { + let member = DuckType::Struct(vec![ + ( + "a".to_owned(), + DuckType::Integer { + width: IntegerWidth::W32, + signed: true, + }, + ), + ("b c".to_owned(), DuckType::List(Box::new(DuckType::Varchar))), + ]); + + assert_eq!(member.name(), "STRUCT(a INTEGER, \"b c\" VARCHAR[])"); + assert_eq!(member.column_type().kind, ColumnKind::Composite); + assert_eq!(member.column_type().catalog, None); + } +} diff --git a/linux/crates/drivers/duckdb/src/editor_session.rs b/linux/crates/drivers/duckdb/src/editor_session.rs new file mode 100644 index 0000000000..0f3dd52f84 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/editor_session.rs @@ -0,0 +1,403 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use async_trait::async_trait; +use tablepro_core::{ + CallOptions, DriverError, EditorSession, EditorSessionId, EditorSessionOptions, EditorSessionState, LivenessPolicy, + LossPhase, ResultSink, RunEnd, RunOptions, RunSummary, TimeoutPhase, TransactionState, UserSql, +}; +use tokio::sync::OwnedMutexGuard; +use tokio::time::Instant; +use tokio_util::sync::CancellationToken; + +use crate::connection::{blocking, stream_into}; +use crate::errors::is_interrupt; +use crate::instance::{Instance, StopHandle}; +use crate::pool::DuckCall; +use crate::result_stream::{StreamOutcome, run_script}; +use crate::stop::interrupt_until_settled; +use crate::transaction_state::transaction_state; + +/// One editor tab's own connection to the database. +/// +/// The tab's transaction and temporary tables live on this connection +/// and nowhere else, so it is never pooled and never reopened behind +/// the user's back. Its connection keeps the database open after the +/// workspace closes. +pub(crate) struct DuckDbEditorSession { + id: EditorSessionId, + shared: Arc, +} + +/// What a run needs once its caller is gone. A run goes on a task of +/// its own, so a dropped caller is a cancel that still interrupts the +/// statement and settles the session. +struct Shared { + call: Arc>>, + stop: StopHandle, + running: AtomicBool, + lost: AtomicBool, + /// Raised to keep the run from starting another statement. + halt: Arc, + statement_timeout: Option, + cancel_grace: Duration, + close_timeout: Duration, +} + +/// Why a run stopped before it finished. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Stop { + Cancelled, + DeadlineElapsed, +} + +impl DuckDbEditorSession { + pub(crate) async fn open( + instance: Arc, + options: EditorSessionOptions, + liveness: &LivenessPolicy, + cancel: CancellationToken, + ) -> Result { + let connecting = blocking(move || instance.link()); + let link = tokio::select! { + biased; + () = cancel.cancelled() => return Err(DriverError::Cancelled), + opened = tokio::time::timeout(liveness.connect_timeout, connecting) => match opened { + Ok(link) => link?, + Err(_) => { + return Err(DriverError::Timeout { + phase: TimeoutPhase::Connect, + server_cancelled: false, + }); + } + }, + }; + let stop = link.stop_handle(); + Ok(Self { + id: EditorSessionId::new(), + shared: Arc::new(Shared { + call: Arc::new(tokio::sync::Mutex::new(Some(DuckCall::own(link)))), + stop, + running: AtomicBool::new(false), + lost: AtomicBool::new(false), + halt: Arc::new(AtomicBool::new(false)), + statement_timeout: options.statement_timeout, + cancel_grace: liveness.cancel_grace, + close_timeout: liveness.close_timeout, + }), + }) + } +} + +impl Shared { + fn state(&self) -> EditorSessionState { + if self.lost.load(Ordering::SeqCst) { + EditorSessionState::Lost + } else if self.running.load(Ordering::SeqCst) { + EditorSessionState::Running + } else { + EditorSessionState::Ready + } + } + + /// Keep the run from starting another statement, and interrupt the + /// one running until it gives way. + async fn stop(&self) { + self.halt.store(true, Ordering::SeqCst); + interrupt_until_settled(&self.stop).await; + } + + /// One run on the connection `held` holds, which it gives up when + /// the connection cannot take another. + async fn run_held( + &self, + mut held: OwnedMutexGuard>, + sql: UserSql, + options: RunOptions, + mut sink: Box, + ) -> Result { + let usable = held + .as_mut() + .filter(|call| call.holds_connection() && !self.lost.load(Ordering::SeqCst)); + let Some(call) = usable else { + self.lose(&mut held); + return Err(DriverError::ConnectionLost { + during: LossPhase::Idle, + }); + }; + let started = Instant::now(); + self.running.store(true, Ordering::SeqCst); + self.halt.store(false, Ordering::SeqCst); + let ended = self.supervise(call, &sql, &options, sink.as_mut()).await; + let transaction_state = match &ended { + Ok(_) => self.state_after_run(call).await, + Err(_) => TransactionState::Unknown, + }; + let kept = call.holds_connection(); + self.running.store(false, Ordering::SeqCst); + let end = match ended { + Ok(end) => end, + Err(error) => { + self.lose(&mut held); + return Err(error); + } + }; + // The state read ran past the cancel grace and kept the + // connection. + if !kept { + self.lose(&mut held); + } + Ok(RunSummary { + elapsed: started.elapsed(), + end, + transaction_state, + }) + } + + /// Run the script, stopping it on a cancel or at the deadline. A + /// stopped statement gets the cancel grace to give way; past that + /// the connection is given up. + async fn supervise( + &self, + call: &mut DuckCall, + sql: &UserSql, + options: &RunOptions, + sink: &mut dyn ResultSink, + ) -> Result { + let deadline = options + .deadline + .or(self.statement_timeout) + .map(|limit| Instant::now() + limit); + if options.cancel.is_cancelled() { + return Ok(RunEnd::Cancelled); + } + // The run reads the cancel and the deadline itself before each + // statement, so one that comes as the run starts sends nothing, + // whichever future the select below polls first. + let halt = self.halt.clone(); + let cancel = options.cancel.clone(); + let due = deadline.map(Instant::into_std); + let halted = move || { + halt.load(Ordering::SeqCst) + || cancel.is_cancelled() + || due.is_some_and(|at| std::time::Instant::now() >= at) + }; + let text = sql.as_str().to_owned(); + let limit = options.row_limit; + let running = stream_into(call, None, sink, move |link, events| { + run_script(link, &text, Some(limit), events, &halted) + }); + tokio::pin!(running); + let stop = tokio::select! { + biased; + () = options.cancel.cancelled() => Stop::Cancelled, + () = elapsed(deadline) => Stop::DeadlineElapsed, + outcome = &mut running => { + return match outcome { + Ok(outcome) if outcome.halted => Ok(halted_end(self.stop_due(options, deadline))), + outcome => finished(outcome), + }; + } + }; + let stopping = async { + let stopped = self.stop(); + tokio::pin!(stopped); + let mut sent = false; + loop { + tokio::select! { + outcome = &mut running => return outcome, + () = &mut stopped, if !sent => sent = true, + } + } + }; + let settled = tokio::time::timeout(self.cancel_grace, stopping).await; + Ok(match (stop, settled) { + (_, Ok(Ok(outcome))) if outcome.halted => halted_end(stop), + (_, Ok(Ok(outcome))) => completed(outcome), + (Stop::Cancelled, Ok(Err(error))) if is_interrupt(&error) => RunEnd::Cancelled, + (Stop::DeadlineElapsed, Ok(Err(error))) if is_interrupt(&error) => { + RunEnd::TimedOut { server_cancelled: true } + } + (_, Ok(Err(error))) => return session_failure(error), + // The statement did not give way in time, and the connection + // goes with it. + (_, Err(_)) => { + return Err(DriverError::ConnectionLost { + during: LossPhase::Statement, + }); + } + }) + } + + /// Give the connection up: nothing runs on the session again. + fn lose(&self, held: &mut Option) { + self.lost.store(true, Ordering::SeqCst); + *held = None; + } + + fn stop_due(&self, options: &RunOptions, deadline: Option) -> Stop { + match deadline.is_some_and(|at| Instant::now() >= at) && !options.cancel.is_cancelled() { + true => Stop::DeadlineElapsed, + false => Stop::Cancelled, + } + } + + /// Where the transaction stands after a run. The answer may not come + /// in time, and then it is unknown rather than guessed. + async fn state_after_run(&self, call: &mut DuckCall) -> TransactionState { + let asking = call.run(None, |link| Ok(transaction_state(link))); + match tokio::time::timeout(self.cancel_grace, asking).await { + Ok(Ok(state)) => state, + Ok(Err(_)) | Err(_) => TransactionState::Unknown, + } + } +} + +/// Marks the session lost unless disarmed: a caller that gives up a +/// call while its work runs leaves the connection with that work. +struct LoseOnDrop<'a>(Option<&'a AtomicBool>); + +impl LoseOnDrop<'_> { + fn disarm(mut self) { + self.0 = None; + } +} + +impl Drop for LoseOnDrop<'_> { + fn drop(&mut self) { + if let Some(lost) = self.0 { + lost.store(true, Ordering::SeqCst); + } + } +} + +async fn elapsed(deadline: Option) { + match deadline { + Some(at) => tokio::time::sleep_until(at).await, + None => std::future::pending().await, + } +} + +fn finished(outcome: Result) -> Result { + match outcome { + Ok(outcome) => Ok(completed(outcome)), + Err(error) => session_failure(error), + } +} + +/// The statement before the stop ended on its own, so DuckDB stopped +/// nothing. +fn halted_end(stop: Stop) -> RunEnd { + match stop { + Stop::Cancelled => RunEnd::Cancelled, + Stop::DeadlineElapsed => RunEnd::TimedOut { + server_cancelled: false, + }, + } +} + +fn completed(outcome: StreamOutcome) -> RunEnd { + match outcome.consumer_gone { + true => RunEnd::Cancelled, + false => RunEnd::Completed, + } +} + +/// A statement DuckDB refused ends the run. A database that is gone +/// ends the session. +fn session_failure(error: DriverError) -> Result { + match error { + DriverError::ConnectionLost { .. } => Err(error), + other => Ok(RunEnd::Failed(other)), + } +} + +#[async_trait] +impl EditorSession for DuckDbEditorSession { + fn id(&self) -> EditorSessionId { + self.id + } + + fn state(&self) -> EditorSessionState { + self.shared.state() + } + + async fn run( + &self, + sql: UserSql, + options: RunOptions, + sink: Box, + ) -> Result { + let Ok(held) = self.shared.call.clone().try_lock_owned() else { + return Err(DriverError::EditorSessionBusy); + }; + // Dropping the caller is itself a cancel: the statement keeps + // running until it is interrupted. + let cancel_on_drop = options.cancel.clone().drop_guard(); + let shared = self.shared.clone(); + let running = tokio::spawn(async move { shared.run_held(held, sql, options, sink).await }); + let outcome = running.await.unwrap_or_else(|_| { + self.shared.running.store(false, Ordering::SeqCst); + self.shared.lost.store(true, Ordering::SeqCst); + Err(DriverError::ConnectionLost { + during: LossPhase::Statement, + }) + }); + drop(cancel_on_drop.disarm()); + outcome + } + + async fn transaction_state(&self, options: CallOptions) -> Result { + let shared = &self.shared; + let mut held = tokio::select! { + biased; + () = options.cancel().cancelled() => return Err(DriverError::Cancelled), + () = elapsed(options.deadline()) => { + return Err(DriverError::Timeout { phase: TimeoutPhase::Statement, server_cancelled: false }); + } + held = shared.call.lock() => held, + }; + let Some(call) = held.as_mut().filter(|call| call.holds_connection()) else { + shared.lose(&mut held); + return Err(DriverError::ConnectionLost { + during: LossPhase::Idle, + }); + }; + let given_up = LoseOnDrop(Some(&shared.lost)); + let state = call.run(None, |link| Ok(transaction_state(link))).await; + given_up.disarm(); + state + } + + async fn close(&self) { + let shared = &self.shared; + shared.lost.store(true, Ordering::SeqCst); + // The run holds the connection until its statement ends and the + // transaction state is read. When that outlasts this wait, the + // connection closes once the session and its run are both gone. + let locking = async { + let stopped = shared.stop(); + tokio::pin!(stopped); + let locked = shared.call.lock(); + tokio::pin!(locked); + let mut sent = false; + loop { + tokio::select! { + held = &mut locked => return held, + () = &mut stopped, if !sent => sent = true, + } + } + }; + let Ok(mut held) = tokio::time::timeout(shared.cancel_grace, locking).await else { + return; + }; + if let Some(call) = held.take() { + let closing = blocking(move || { + drop(call); + Ok(()) + }); + let _ = tokio::time::timeout(shared.close_timeout, closing).await; + } + } +} diff --git a/linux/crates/drivers/duckdb/src/encode.rs b/linux/crates/drivers/duckdb/src/encode.rs new file mode 100644 index 0000000000..a601547051 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/encode.rs @@ -0,0 +1,199 @@ +//! Values as DuckDB takes them in: bound as parameters, or as the text +//! a literal casts. +//! +//! A value DuckDB has a binding for goes as itself. The rest go as text +//! the dialect casts, which DuckDB parses exactly: a decimal wider than +//! any binding, a nanosecond timestamp, a time with an offset. + +use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike}; +use duckdb::types::{TimeUnit, Value as Bound}; +use tablepro_core::DriverError; +use tablepro_core::export::value_to_text; +use tablepro_core::statement::BoundParam; +use tablepro_core::value::{SqlTime, Temporal, Value}; + +const UNIX_EPOCH_DAYS_FROM_CE: i32 = 719_163; + +pub(crate) fn bind_all(params: &[BoundParam]) -> Result, DriverError> { + params.iter().map(|param| bind(param.value())).collect() +} + +pub(crate) fn bind(value: &Value) -> Result { + Ok(match value { + Value::Null => Bound::Null, + Value::Bool(flag) => Bound::Boolean(*flag), + Value::Int(integer) => Bound::BigInt(*integer), + Value::UInt(integer) => Bound::UBigInt(*integer), + Value::WideInt(integer) => match (integer.to_i128(), integer.to_u128()) { + (Some(signed), _) => Bound::HugeInt(signed), + (None, Some(unsigned)) => Bound::UHugeInt(unsigned), + (None, None) => Bound::Text(integer.to_string()), + }, + Value::Float32(real) => Bound::Float(*real), + Value::Float64(real) => Bound::Double(*real), + Value::Bytes(bytes) => Bound::Blob(bytes.clone()), + Value::Date(Temporal::Finite(day)) => Bound::Date32(day.num_days_from_ce() - UNIX_EPOCH_DAYS_FROM_CE), + Value::Time(time) if time.total_nanos() % 1_000 == 0 => match time.total_micros() { + Some(micros) if micros >= 0 => Bound::Time64(TimeUnit::Microsecond, micros), + _ => text(value)?, + }, + // The crate binds nanoseconds, which hold a smaller span than + // DuckDB's microseconds; a longer one goes as DuckDB's text. + Value::Interval(interval) => match interval.microseconds.checked_mul(1_000) { + Some(nanos) => Bound::Interval { + months: interval.months, + days: interval.days, + nanos, + }, + None => text(value)?, + }, + Value::Undecodable(_) => { + return Err(DriverError::Protocol( + "a value the driver could not read was bound".to_owned(), + )); + } + _ => text(value)?, + }) +} + +fn text(value: &Value) -> Result { + duck_text(value) + .map(Bound::Text) + .ok_or_else(|| DriverError::Protocol(format!("a {} value has no text", value.variant_name()))) +} + +/// The text DuckDB reads back as `value` when cast to the value's type. +pub(crate) fn duck_text(value: &Value) -> Option { + Some(match value { + Value::Null | Value::Undecodable(_) => return None, + Value::Other(other) => other.text.clone(), + Value::Date(day) => temporal(day, |day| date_text(*day)), + Value::Time(time) => time_text(time), + Value::TimeTz(time) => format!( + "{}{}", + time_text(&time.time), + offset_text(time.offset.local_minus_utc()) + ), + Value::Timestamp(stamp) => temporal(stamp, stamp_text), + Value::TimestampTz(stamp) => temporal(stamp, |stamp| { + let at = stamp.to_datetime(); + format!( + "{}{}", + stamp_text(&at.naive_local()), + offset_text(at.offset().local_minus_utc()) + ) + }), + Value::Interval(interval) => { + crate::decode::interval_text(interval.months, interval.days, interval.microseconds) + } + other => value_to_text(other)?, + }) +} + +fn temporal(value: &Temporal, finite: impl Fn(&T) -> String) -> String { + match value { + Temporal::Finite(inner) => finite(inner), + Temporal::Infinity => "infinity".to_owned(), + Temporal::NegInfinity => "-infinity".to_owned(), + } +} + +/// A date as DuckDB parses one, a year before 1 written `(BC)`: DuckDB +/// reads a plain `BC` as nothing and keeps the year. +pub(crate) fn date_text(day: NaiveDate) -> String { + let year = day.year(); + match year <= 0 { + true => format!("{:04}-{:02}-{:02} (BC)", 1 - year, day.month(), day.day()), + false => format!("{year:04}-{:02}-{:02}", day.month(), day.day()), + } +} + +fn time_text(time: &SqlTime) -> String { + time.format(None) +} + +/// A timestamp as DuckDB parses one, to the nanosecond. +pub(crate) fn stamp_text(stamp: &NaiveDateTime) -> String { + let nanos = stamp.nanosecond(); + let fraction = match nanos { + 0 => String::new(), + nanos => format!(".{}", format!("{nanos:09}").trim_end_matches('0')), + }; + format!( + "{} {:02}:{:02}:{:02}{fraction}", + date_text(stamp.date()), + stamp.hour(), + stamp.minute(), + stamp.second() + ) +} + +fn offset_text(seconds: i32) -> String { + let sign = if seconds < 0 { '-' } else { '+' }; + let magnitude = seconds.unsigned_abs(); + let (hours, minutes, seconds) = (magnitude / 3_600, magnitude / 60 % 60, magnitude % 60); + match seconds { + 0 => format!("{sign}{hours:02}:{minutes:02}"), + _ => format!("{sign}{hours:02}:{minutes:02}:{seconds:02}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_timestamp_is_text_duckdb_reads_back() { + let stamp = NaiveDate::from_ymd_opt(2024, 2, 29) + .unwrap() + .and_hms_nano_opt(12, 0, 1, 123_456_789) + .unwrap(); + let bc = NaiveDate::from_ymd_opt(-43, 3, 15) + .unwrap() + .and_hms_opt(12, 0, 0) + .unwrap(); + + assert_eq!(stamp_text(&stamp), "2024-02-29 12:00:01.123456789"); + assert_eq!(stamp_text(&bc), "0044-03-15 (BC) 12:00:00"); + assert_eq!(offset_text(-19_800), "-05:30"); + assert_eq!(offset_text(1_172), "+00:19:32"); + } + + #[test] + fn an_interval_too_long_for_nanoseconds_goes_as_text() { + let long = tablepro_core::value::SqlInterval { + months: 0, + days: 0, + microseconds: 10_000_000_000_000_000, + }; + let short = tablepro_core::value::SqlInterval { + months: 1, + days: 2, + microseconds: 3, + }; + + assert!(matches!(bind(&Value::Interval(long)).unwrap(), Bound::Text(_))); + assert!(matches!( + bind(&Value::Interval(short)).unwrap(), + Bound::Interval { + months: 1, + days: 2, + nanos: 3_000 + } + )); + } + + #[test] + fn integers_too_wide_for_a_binding_go_as_text() { + let wide: tablepro_core::value::WideInteger = + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + .parse() + .unwrap(); + + assert!(matches!(bind(&Value::WideInt(wide)).unwrap(), Bound::Text(_))); + assert_eq!( + bind(&Value::WideInt(tablepro_core::value::WideInteger::from_i128(-5))).unwrap(), + Bound::HugeInt(-5) + ); + } +} diff --git a/linux/crates/drivers/duckdb/src/errors.rs b/linux/crates/drivers/duckdb/src/errors.rs new file mode 100644 index 0000000000..7ccad71b03 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/errors.rs @@ -0,0 +1,207 @@ +//! DuckDB's errors as the app's. +//! +//! DuckDB numbers nothing: every message starts with the kind of error, +//! as in "Constraint Error: ...", and the kind is what these branches +//! turn on. The C API's own code is always the same failure flag. + +use std::path::Path; + +use tablepro_core::{BusyReason, DriverError, LossPhase, ReadOnlyRefusal, ServerCode, ServerDiagnostics}; + +const INTERRUPT: &str = "INTERRUPT"; +const PARSER: &str = "Parser"; + +/// A message split into its kind and the rest, where it has a kind. +fn split_kind(message: &str) -> (Option<&str>, &str) { + match message.split_once(" Error: ") { + Some((kind, rest)) if !kind.is_empty() && kind.len() <= 40 && !kind.contains('\n') => (Some(kind), rest), + _ => (None, message), + } +} + +/// An error from a statement, or from DuckDB around one. +pub(crate) fn map_error(error: duckdb::Error) -> DriverError { + match error { + duckdb::Error::DuckDBFailure(_, Some(message)) => from_message(&message), + duckdb::Error::DuckDBFailure(failure, None) => DriverError::server(failure.to_string()), + duckdb::Error::FromSqlConversionFailure(index, kind, detail) => DriverError::Decode { + column: index.to_string(), + detail: format!("{kind}: {detail}"), + }, + duckdb::Error::Utf8Error(detail) => DriverError::Decode { + column: String::new(), + detail: detail.to_string(), + }, + duckdb::Error::NulError(_) => DriverError::server("the SQL holds a NUL character"), + other => DriverError::Protocol(other.to_string()), + } +} + +/// What DuckDB said, classified by its kind. +pub(crate) fn from_message(message: &str) -> DriverError { + let (kind, rest) = split_kind(message); + let diagnostics = ServerDiagnostics::new( + kind.map(|kind| ServerCode::DuckDb { + error_type: kind.to_owned(), + }), + rest, + ); + match kind { + Some("FATAL") => DriverError::ConnectionLost { + during: LossPhase::Statement, + }, + Some("Invalid Input") if rest.contains("read-only mode") => { + DriverError::ReadOnly(ReadOnlyRefusal::server(diagnostics)) + } + Some("TransactionContext") if rest.contains("read-only mode") => { + DriverError::ReadOnly(ReadOnlyRefusal::server(diagnostics)) + } + // A database that hit a fatal error refuses everything after it. + _ if rest.contains("database has been invalidated") => DriverError::ConnectionLost { + during: LossPhase::Statement, + }, + _ => DriverError::reported(diagnostics), + } +} + +/// An error while opening `path`, where the file itself is usually the +/// answer. +pub(crate) fn map_open_error(error: duckdb::Error, path: &Path) -> DriverError { + let duckdb::Error::DuckDBFailure(_, Some(message)) = &error else { + return map_error(error); + }; + let (kind, rest) = split_kind(message); + match kind { + Some("IO") if rest.contains("Could not set lock on file") => DriverError::Busy(BusyReason::FileLocked), + Some("IO") if rest.contains("not a valid DuckDB database file") => DriverError::NotADatabase { + path: path.to_path_buf(), + }, + Some("IO") if rest.contains("Permission denied") => DriverError::FileAccessDenied { + path: path.to_path_buf(), + }, + _ => map_error(error), + } +} + +/// Whether the error is DuckDB stopping a statement it was told to +/// interrupt. +pub(crate) fn is_interrupt(error: &DriverError) -> bool { + match error { + DriverError::Server(diagnostics) => matches!( + &diagnostics.code, + Some(ServerCode::DuckDb { error_type }) if error_type == INTERRUPT + ), + DriverError::RolledBack { source, .. } => is_interrupt(source), + _ => false, + } +} + +/// Whether DuckDB refused the text of a statement before running it. +pub(crate) fn is_parse_error(error: &DriverError) -> bool { + match error { + DriverError::Server(diagnostics) => matches!( + &diagnostics.code, + Some(ServerCode::DuckDb { error_type }) if error_type == PARSER + ), + _ => false, + } +} + +/// The error a stopped statement ends with, for a stop that came before +/// the statement was sent. +pub(crate) fn interrupted() -> DriverError { + from_message("INTERRUPT Error: Interrupted!") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_message_keeps_its_kind_as_the_code() { + let error = from_message("Constraint Error: PRIMARY KEY or UNIQUE constraint violation: duplicate key \"1\""); + + let DriverError::Server(diagnostics) = error else { + panic!("not a server error: {error:?}"); + }; + assert_eq!( + diagnostics.code, + Some(ServerCode::DuckDb { + error_type: "Constraint".to_owned() + }) + ); + assert_eq!( + diagnostics.message, + "PRIMARY KEY or UNIQUE constraint violation: duplicate key \"1\"" + ); + } + + #[test] + fn a_write_on_a_read_only_database_is_the_servers_refusal() { + let attached = from_message( + "Invalid Input Error: Cannot execute statement of type \"INSERT\" on database \"ro\" which is attached in read-only mode!", + ); + let transaction = from_message( + "TransactionContext Error: Cannot write to database \"ro\" - transaction is launched in read-only mode", + ); + + assert!(matches!( + attached, + DriverError::ReadOnly(ReadOnlyRefusal::ServerRejected(_)) + )); + assert!(matches!( + transaction, + DriverError::ReadOnly(ReadOnlyRefusal::ServerRejected(_)) + )); + } + + #[test] + fn an_interrupt_is_told_apart_from_other_errors() { + assert!(is_interrupt(&interrupted())); + assert!(is_interrupt(&DriverError::RolledBack { + statement_index: 0, + source: Box::new(interrupted()), + })); + assert!(!is_interrupt(&from_message( + "Parser Error: syntax error at end of input" + ))); + assert!(!is_interrupt(&DriverError::Cancelled)); + } + + #[test] + fn a_message_without_a_kind_is_kept_whole() { + let DriverError::Server(diagnostics) = from_message("something odd") else { + panic!("not a server error"); + }; + assert_eq!(diagnostics.code, None); + assert_eq!(diagnostics.message, "something odd"); + } + + #[test] + fn opening_a_file_names_what_is_wrong_with_it() { + let path = Path::new("/srv/data.duckdb"); + let failure = + |message: &str| duckdb::Error::DuckDBFailure(duckdb::ffi::Error::new(1), Some(message.to_owned())); + + assert_eq!( + map_open_error( + failure("IO Error: Could not set lock on file \"/srv/data.duckdb\": Conflicting lock is held"), + path + ), + DriverError::Busy(BusyReason::FileLocked) + ); + assert_eq!( + map_open_error( + failure("IO Error: The file \"/srv/data.duckdb\" exists, but it is not a valid DuckDB database file!"), + path + ), + DriverError::NotADatabase { + path: path.to_path_buf() + } + ); + assert!(matches!( + map_open_error(failure("FATAL Error: something broke"), path), + DriverError::ConnectionLost { .. } + )); + } +} diff --git a/linux/crates/drivers/duckdb/src/instance.rs b/linux/crates/drivers/duckdb/src/instance.rs new file mode 100644 index 0000000000..1b41124ade --- /dev/null +++ b/linux/crates/drivers/duckdb/src/instance.rs @@ -0,0 +1,440 @@ +//! One DuckDB database per file in this process. +//! +//! Every connection to a file comes from one instance: the workspace's +//! pool, each editor tab and each streamed read. A second instance on +//! the same file would keep a catalogue and a write-ahead log of its +//! own, and DuckDB's file lock does not keep two instances of one +//! process apart, so two windows on the file would overwrite each +//! other's changes. A file is known by its device and inode, so a hard +//! link or a second path to it is the same file, and one attached to +//! another open database counts as open. Every connection holds its +//! instance, which closes the file when the last of them is gone, +//! editor sessions included. + +use std::collections::HashMap; +use std::os::unix::fs::MetadataExt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock, Mutex, PoisonError, Weak}; +use std::time::Duration; + +use duckdb::{AccessMode, Config, InterruptHandle}; +use tablepro_core::{BusyReason, DriverError, FileEndpoint, FileOpenMode, LossPhase}; + +use crate::create_file::create_or_replace; +use crate::errors::{map_error, map_open_error}; + +/// The files this process has open. An entry whose instance is gone is +/// a database still closing: its drop removes the entry once DuckDB has +/// let the file go. +static OPEN: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); + +/// A file whatever path names it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct FileId { + device: u64, + inode: u64, +} + +impl FileId { + fn of(path: &Path) -> Option { + std::fs::metadata(path).ok().map(|metadata| Self { + device: metadata.dev(), + inode: metadata.ino(), + }) + } +} + +struct Opened { + instance: Weak, + read_only: bool, + /// The instance's root connection, which asks what it has attached + /// without keeping the instance alive. + root: Root, +} + +type Root = Arc>>; + +/// How long an open waits before it looks again at a file still +/// closing. +const CLOSING_POLL: Duration = Duration::from_millis(10); + +/// An open database file, shared by every connection to it. +pub(crate) struct Instance { + /// What new connections are cloned from, since DuckDB connects only + /// to a database already open. It runs only the catalogue read of + /// what is attached. Taken at drop, which closes the file. + root: Root, + id: FileId, + path: PathBuf, + read_only: bool, + /// The catalogue name DuckDB gave the file, which the catalogue + /// reads filter on: attached databases are in the same lists. + catalog: String, +} + +impl std::fmt::Debug for Instance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Instance") + .field("path", &self.path) + .field("read_only", &self.read_only) + .field("catalog", &self.catalog) + .finish_non_exhaustive() + } +} + +/// DuckDB checkpoints the file as its last connection closes. An open of +/// the same file waits until that is done, or it would read a file still +/// being written. +impl Drop for Instance { + fn drop(&mut self) { + let mut open = OPEN.lock().unwrap_or_else(PoisonError::into_inner); + drop(self.root.lock().unwrap_or_else(PoisonError::into_inner).take()); + if open + .get(&self.id) + .is_some_and(|entry| entry.instance.strong_count() == 0) + { + open.remove(&self.id); + } + } +} + +/// One connection to an instance, with the handle that interrupts the +/// statement it runs. Declared before the instance, so the connection +/// closes first. +pub(crate) struct Link { + pub(crate) connection: duckdb::Connection, + stop: StopHandle, + _instance: Arc, +} + +/// What stops the statement a connection runs. DuckDB clears an +/// interrupt when its next statement starts, so one sent just before a +/// statement begins is lost: a stop keeps sending it for as long as the +/// connection runs work a stop may cut short. +#[derive(Clone)] +pub(crate) struct StopHandle { + interrupt: Arc, + stoppable: Arc, +} + +impl StopHandle { + pub(crate) fn interrupt(&self) { + self.interrupt.interrupt(); + } + + pub(crate) fn is_stoppable(&self) -> bool { + self.stoppable.load(Ordering::SeqCst) + } + + pub(crate) fn set_stoppable(&self, stoppable: bool) { + self.stoppable.store(stoppable, Ordering::SeqCst); + } +} + +impl std::fmt::Debug for Link { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Link").finish_non_exhaustive() + } +} + +impl Link { + fn new(connection: duckdb::Connection, instance: Arc) -> Self { + let stop = StopHandle { + interrupt: connection.interrupt_handle(), + stoppable: Arc::new(AtomicBool::new(false)), + }; + Self { + connection, + stop, + _instance: instance, + } + } + + pub(crate) fn stop_handle(&self) -> StopHandle { + self.stop.clone() + } + + /// Keep stops off the statements that clean up after work: a + /// rollback must not be interrupted by the stop that caused it. + pub(crate) fn hold_stops(&self) { + self.stop.set_stoppable(false); + } +} + +impl Instance { + /// Open the file the endpoint names, or join the instance this + /// process already has on it. Blocks: DuckDB reads the file and + /// replays its log here, and a file still closing is waited for. + pub(crate) fn open(endpoint: &FileEndpoint, read_only: bool) -> Result, DriverError> { + let path = endpoint.path(); + loop { + // Held through the open, so two windows opening one file at + // once get one instance. + let mut open = OPEN.lock().unwrap_or_else(PoisonError::into_inner); + let known = FileId::of(path) + .and_then(|id| open.get(&id)) + .map(|entry| entry.instance.strong_count() > 0); + match (known, endpoint.mode()) { + (Some(false), _) => { + drop(open); + std::thread::sleep(CLOSING_POLL); + continue; + } + // Replacing a file this process has open would pull it + // out from under the windows using it. + (Some(true), FileOpenMode::CreateNew) => return Err(DriverError::Busy(BusyReason::FileLocked)), + (None, FileOpenMode::CreateNew) => { + if FileId::of(path).is_some_and(|id| attached(&open, id)) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + create_or_replace(path)?; + } + (_, FileOpenMode::OpenExisting) => {} + } + let canonical = existing_file(path)?; + let id = FileId::of(&canonical).ok_or_else(|| DriverError::FileNotFound { + path: path.to_path_buf(), + })?; + if let Some(entry) = open.get(&id) { + // One instance is opened one way, and a read-only + // connection cannot be told apart on one opened to write. + if entry.read_only != read_only { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + match entry.instance.upgrade() { + Some(instance) => return Ok(instance), + None => { + drop(open); + std::thread::sleep(CLOSING_POLL); + continue; + } + } + } + if attached(&open, id) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + let root = duckdb::Connection::open_with_flags(&canonical, config(read_only)?) + .map_err(|error| map_open_error(error, path))?; + let catalog = root + .query_row("SELECT current_database()", [], |row| row.get::<_, String>(0)) + .map_err(|error| map_open_error(error, path))?; + let root: Root = Arc::new(Mutex::new(Some(root))); + let instance = Arc::new(Self { + root: root.clone(), + id, + path: canonical, + read_only, + catalog, + }); + open.insert( + id, + Opened { + instance: Arc::downgrade(&instance), + read_only, + root, + }, + ); + return Ok(instance); + } + } + + /// A new connection to the database, as DuckDB documents for using + /// one database from several places. + pub(crate) fn link(self: &Arc) -> Result { + let root = self.root.lock().unwrap_or_else(PoisonError::into_inner); + let Some(root) = root.as_ref() else { + return Err(DriverError::ConnectionLost { + during: LossPhase::Idle, + }); + }; + let connection = root.try_clone().map_err(map_error)?; + Ok(Link::new(connection, self.clone())) + } + + pub(crate) fn catalog(&self) -> &str { + &self.catalog + } +} + +/// Whether an open database has the file attached, which DuckDB lists +/// with its own file. +fn attached(open: &HashMap, id: FileId) -> bool { + open.values().any(|entry| { + let root = entry.root.lock().unwrap_or_else(PoisonError::into_inner); + let Some(root) = root.as_ref() else { + return false; + }; + let paths = root + .prepare("SELECT path FROM duckdb_databases() WHERE path IS NOT NULL") + .and_then(|mut statement| { + statement + .query_map([], |row| row.get::<_, String>(0)) + .and_then(|rows| rows.collect::, _>>()) + }); + match paths { + Ok(paths) => paths.iter().any(|path| FileId::of(Path::new(path)) == Some(id)), + Err(error) => { + tracing::warn!(%error, "could not read the databases an open file has attached"); + false + } + } + }) +} + +/// Run `attach`, an ATTACH that may open any of `targets`, unless one of +/// them is open in this process, as a workspace's database or attached +/// to one: DuckDB would make a second database on it, which writes past +/// the first. The registry is held across the check and the attach, so +/// no window opens the file, and no tab attaches it, in between. +pub(crate) fn attach_unless_open( + targets: &[PathBuf], + attach: impl FnOnce() -> Result, +) -> Result { + let open = OPEN.lock().unwrap_or_else(PoisonError::into_inner); + let in_use = targets + .iter() + .filter_map(|target| FileId::of(target)) + .any(|id| open.contains_key(&id) || attached(&open, id)); + if in_use { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + let result = attach(); + drop(open); + result +} + +/// Every connection opens the same way. Extensions are never fetched: +/// a query that needs one DuckDB does not link in fails instead of +/// downloading it, as the app makes no requests of its own. +fn config(read_only: bool) -> Result { + let mode = match read_only { + true => AccessMode::ReadOnly, + false => AccessMode::ReadWrite, + }; + Config::default() + .access_mode(mode) + .and_then(|config| config.with("autoinstall_known_extensions", "false")) + .and_then(|config| config.with("autoload_known_extensions", "false")) + .map_err(map_error) +} + +/// DuckDB creates a file it is asked to open and cannot find, so a moved +/// database would come back as a new empty one. Only a file that is +/// there is opened. +fn existing_file(path: &Path) -> Result { + let canonical = std::fs::canonicalize(path).map_err(|error| file_error(error.kind(), path))?; + match std::fs::metadata(&canonical) { + Ok(metadata) if metadata.is_dir() => Err(DriverError::NotADatabase { + path: path.to_path_buf(), + }), + Ok(_) => Ok(canonical), + Err(error) => Err(file_error(error.kind(), path)), + } +} + +pub(crate) fn file_error(kind: std::io::ErrorKind, path: &Path) -> DriverError { + let path = path.to_path_buf(); + match kind { + std::io::ErrorKind::NotFound => DriverError::FileNotFound { path }, + std::io::ErrorKind::IsADirectory | std::io::ErrorKind::NotADirectory => DriverError::NotADatabase { path }, + std::io::ErrorKind::PermissionDenied => DriverError::FileAccessDenied { path }, + kind => DriverError::FileIo { path, kind }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn created(directory: &Path, name: &str) -> FileEndpoint { + FileEndpoint::create_new(directory.join(name)).unwrap() + } + + #[test] + fn a_missing_file_is_not_created() { + let directory = tempfile::tempdir().unwrap(); + let missing = directory.path().join("moved.duckdb"); + + for read_only in [false, true] { + let opened = Instance::open(&FileEndpoint::open_existing(missing.clone()).unwrap(), read_only); + + assert_eq!(opened.unwrap_err(), DriverError::FileNotFound { path: missing.clone() }); + assert!(!missing.exists(), "opening created the file with read_only={read_only}"); + } + } + + #[test] + fn one_file_has_one_instance_in_the_process() { + let directory = tempfile::tempdir().unwrap(); + let first = Instance::open(&created(directory.path(), "one.duckdb"), false).unwrap(); + let again = FileEndpoint::open_existing(directory.path().join("one.duckdb")).unwrap(); + + let second = Instance::open(&again, false).unwrap(); + + assert!(Arc::ptr_eq(&first, &second)); + assert_eq!( + Instance::open(&again, true).unwrap_err(), + DriverError::Busy(BusyReason::FileLocked), + "a read-only window joined a writable instance" + ); + } + + /// A window that closes while a tab still runs on the file leaves + /// the file to that tab, and a window opening it again joins it. + #[test] + fn a_connection_keeps_its_instance_open() { + let directory = tempfile::tempdir().unwrap(); + let first = Instance::open(&created(directory.path(), "kept.duckdb"), false).unwrap(); + let link = first.link().unwrap(); + let weak = Arc::downgrade(&first); + drop(first); + + let again = Instance::open( + &FileEndpoint::open_existing(directory.path().join("kept.duckdb")).unwrap(), + false, + ) + .unwrap(); + + assert!(weak.upgrade().is_some_and(|kept| Arc::ptr_eq(&kept, &again))); + drop((link, again)); + assert!(weak.upgrade().is_none()); + assert!( + Instance::open(&created(directory.path(), "kept.duckdb"), false).is_ok(), + "the file was still held once every connection was gone" + ); + } + + #[test] + fn a_file_is_not_replaced_while_this_process_has_it_open() { + let directory = tempfile::tempdir().unwrap(); + let endpoint = created(directory.path(), "busy.duckdb"); + let _open = Instance::open(&endpoint, false).unwrap(); + + assert_eq!( + Instance::open(&endpoint, false).unwrap_err(), + DriverError::Busy(BusyReason::FileLocked) + ); + } + + #[test] + fn the_catalogue_is_named_after_the_file() { + let directory = tempfile::tempdir().unwrap(); + + let instance = Instance::open(&created(directory.path(), "sales.duckdb"), false).unwrap(); + + assert_eq!(instance.catalog(), "sales"); + assert!(!instance.read_only); + } + + #[test] + fn a_file_that_is_not_a_database_is_named_as_such() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("notes.duckdb"); + std::fs::write(&path, b"plain text, longer than any header DuckDB reads first").unwrap(); + + let opened = Instance::open(&FileEndpoint::open_existing(path.clone()).unwrap(), true); + + assert_eq!(opened.unwrap_err(), DriverError::NotADatabase { path }); + } +} diff --git a/linux/crates/drivers/duckdb/src/lib.rs b/linux/crates/drivers/duckdb/src/lib.rs new file mode 100644 index 0000000000..73ae16e90a --- /dev/null +++ b/linux/crates/drivers/duckdb/src/lib.rs @@ -0,0 +1,26 @@ +//! The DuckDB driver: one database file, opened once per process, with +//! a pool of connections to it for the workspace and a connection of +//! its own for each editor tab and each streamed read. + +mod capabilities; +mod catalog; +mod connection; +mod create_file; +mod ddl; +mod decode; +mod dialect; +mod driver; +mod duck_type; +mod editor_session; +mod encode; +mod errors; +mod instance; +mod pool; +mod result_stream; +mod stop; +mod transaction_state; +mod write_batch; + +pub use dialect::DuckDbDialect; +pub use driver::DuckDbDriver; +pub use pool::WORKSPACE_POOL_MAX_SIZE; diff --git a/linux/crates/drivers/duckdb/src/pool.rs b/linux/crates/drivers/duckdb/src/pool.rs new file mode 100644 index 0000000000..312ee7fc9c --- /dev/null +++ b/linux/crates/drivers/duckdb/src/pool.rs @@ -0,0 +1,230 @@ +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +use deadpool::Runtime; +use deadpool::managed::{Manager, Metrics, Object, Pool, PoolError, RecycleResult, Timeouts}; +use tablepro_core::{BusyReason, CallResource, DriverError, LivenessPolicy, LossPhase, StopRequest}; + +use crate::instance::{Instance, Link, StopHandle}; + +/// The workspace pool's size. DuckDB runs the queries of one database +/// side by side, so a slow page read does not hold up the sidebar. +pub const WORKSPACE_POOL_MAX_SIZE: usize = 4; + +pub(crate) type LinkPool = Pool; + +/// deadpool's manager for workspace connections, which are new +/// connections to the one open database. It does not keep the database +/// open: a workspace that closed lets the file go. +pub(crate) struct LinkManager { + instance: Weak, +} + +impl Manager for LinkManager { + type Type = Link; + type Error = DriverError; + + async fn create(&self) -> Result { + match self.instance.upgrade() { + Some(instance) => instance.link(), + None => Err(DriverError::ConnectionLost { + during: LossPhase::Idle, + }), + } + } + + /// Every call ends its own transaction, and one that did not end + /// cleanly never gets here: the call takes it out of the pool. + async fn recycle(&self, _link: &mut Link, _metrics: &Metrics) -> RecycleResult { + Ok(()) + } +} + +pub(crate) fn build_pool(instance: &Arc, liveness: &LivenessPolicy) -> Result { + Pool::builder(LinkManager { + instance: Arc::downgrade(instance), + }) + .max_size(WORKSPACE_POOL_MAX_SIZE) + .wait_timeout(Some(liveness.pool_wait)) + .create_timeout(Some(liveness.connect_timeout)) + .runtime(Runtime::Tokio1) + .build() + .map_err(|error| DriverError::Protocol(error.to_string())) +} + +/// The probe never waits for a slot: a busy pool is not a dead database. +pub(crate) fn probe_timeouts(liveness: &LivenessPolicy) -> Timeouts { + Timeouts { + wait: Some(Duration::ZERO), + create: Some(liveness.connect_timeout), + recycle: None, + } +} + +fn map_pool_error(error: PoolError) -> DriverError { + match error { + PoolError::Backend(error) => error, + PoolError::Timeout(_) => DriverError::Busy(BusyReason::NoFreeConnection), + PoolError::Closed => DriverError::ConnectionLost { + during: LossPhase::Idle, + }, + other => DriverError::Protocol(other.to_string()), + } +} + +/// Where a call's connection lives while the call holds it. +enum Held { + Empty, + Pooled(Object), + Own(Link), +} + +/// The connection one call runs on: a pool slot, or a connection of the +/// call's own for a probe that finds every slot busy and for a streamed +/// read. It starts empty and is filled inside the call, so the call's +/// deadline and cancel cover the wait for a slot too. +pub(crate) struct DuckCall { + held: Held, + abandon: Arc, +} + +impl DuckCall { + pub(crate) fn new() -> Self { + Self { + held: Held::Empty, + abandon: Arc::new(AtomicBool::new(false)), + } + } + + pub(crate) fn pooled(object: Object) -> Self { + Self { + held: Held::Pooled(object), + abandon: Arc::new(AtomicBool::new(false)), + } + } + + /// The connection may still hold a transaction, so it must not go + /// back to the pool. + pub(crate) fn taint(&self) { + self.abandon.store(true, Ordering::SeqCst); + } + + pub(crate) fn own(link: Link) -> Self { + Self { + held: Held::Own(link), + abandon: Arc::new(AtomicBool::new(false)), + } + } + + /// Take a slot, giving up as soon as the call is stopped. + pub(crate) async fn acquire( + &mut self, + pool: &LinkPool, + timeouts: &Timeouts, + request: &StopRequest, + ) -> Result<(), DriverError> { + let object = tokio::select! { + biased; + () = request.requested() => return Err(crate::errors::interrupted()), + object = pool.timeout_get(timeouts) => object.map_err(map_pool_error)?, + }; + self.held = Held::Pooled(object); + Ok(()) + } + + /// Whether the call still has its connection. A `run` given up while + /// its work ran leaves the connection with that work. + pub(crate) fn holds_connection(&self) -> bool { + !matches!(self.held, Held::Empty) + } + + /// The handle that stops the statement this call runs. + pub(crate) fn stop_handle(&self) -> Option { + match &self.held { + Held::Pooled(object) => Some(object.stop_handle()), + Held::Own(link) => Some(link.stop_handle()), + Held::Empty => None, + } + } + + /// Run `work` on the held connection, on a thread where blocking is + /// allowed, unless `request` asks for a stop by the time it would + /// start. A stop interrupts it until it returns or holds stops off + /// itself. A call abandoned while the work runs never returns its + /// connection to the pool. + pub(crate) async fn run(&mut self, request: Option<&StopRequest>, work: W) -> Result + where + T: Send + 'static, + W: FnOnce(&mut Link) -> Result + Send + 'static, + { + let held = std::mem::replace(&mut self.held, Held::Empty); + let Some(stop) = (match &held { + Held::Pooled(object) => Some(object.stop_handle()), + Held::Own(link) => Some(link.stop_handle()), + Held::Empty => None, + }) else { + return Err(DriverError::Protocol("the call holds no connection".to_owned())); + }; + let abandon = self.abandon.clone(); + let request = request.cloned(); + stop.set_stoppable(true); + let joined = tokio::task::spawn_blocking(move || { + let mut held = held; + let stopped = request.as_ref().is_some_and(StopRequest::is_requested); + let outcome = match (&mut held, stopped) { + (_, true) => Ok(Err(crate::errors::interrupted())), + (Held::Pooled(object), false) => catch_unwind(AssertUnwindSafe(|| work(object))), + (Held::Own(link), false) => catch_unwind(AssertUnwindSafe(|| work(link))), + (Held::Empty, false) => Ok(Err(DriverError::Protocol("the call holds no connection".to_owned()))), + }; + stop.set_stoppable(false); + match outcome { + Ok(result) if !abandon.load(Ordering::SeqCst) => (held, result), + // A connection whose work broke off, or whose caller gave + // it up, holds a state nothing knows. + Ok(result) => (detach(held), result), + Err(_) => ( + detach(held), + Err(DriverError::Protocol("DuckDB failed inside a call".to_owned())), + ), + } + }) + .await; + match joined { + Ok((held, result)) => { + self.held = held; + result + } + Err(_) => Err(DriverError::ConnectionLost { + during: LossPhase::Statement, + }), + } + } +} + +fn detach(held: Held) -> Held { + if let Held::Pooled(object) = held { + drop(Object::take(object)); + } + Held::Empty +} + +impl CallResource for DuckCall { + fn abandon_flag(&self) -> Arc { + self.abandon.clone() + } + + fn discard(mut self) { + self.held = detach(std::mem::replace(&mut self.held, Held::Empty)); + } +} + +impl Drop for DuckCall { + fn drop(&mut self) { + if self.abandon.load(Ordering::SeqCst) { + self.held = detach(std::mem::replace(&mut self.held, Held::Empty)); + } + } +} diff --git a/linux/crates/drivers/duckdb/src/result_stream.rs b/linux/crates/drivers/duckdb/src/result_stream.rs new file mode 100644 index 0000000000..c5d0ded8d4 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/result_stream.rs @@ -0,0 +1,626 @@ +//! Running statements on a connection and reading what they return. +//! +//! Everything here blocks: it runs on a thread of the blocking pool +//! with a connection the call holds. A result is read a chunk at a time +//! off DuckDB's streaming result, so a read stops where the reader stops +//! instead of DuckDB producing the whole result first. + +use std::ffi::{OsStr, OsString}; +use std::path::PathBuf; +use std::sync::Arc; + +use duckdb::arrow::array::{Array, StructArray}; +use tablepro_core::column::ResultColumn; +use tablepro_core::sql_syntax::SqlGrammar; +use tablepro_core::sql_syntax::script::{ + LexicalSettings, OpenConstruct, ScriptDiagnostic, ScriptPlan, ScriptToken, ScriptTokenKind, +}; +use tablepro_core::statement::{BoundParam, Statement}; +use tablepro_core::value::Value; +use tablepro_core::{DriverError, QueryResult, ResultEvent, RowLimit, Truncation}; +use tokio::sync::mpsc; + +use crate::decode::value_at; +use crate::duck_type::DuckType; +use crate::encode::bind_all; +use crate::errors::{is_parse_error, map_error}; +use crate::instance::{Link, attach_unless_open}; + +/// How many rows travel in one `Rows` event: enough that a large read +/// is not one await per row, few enough that the first rows show at +/// once. +const ROWS_PER_EVENT: usize = 256; + +/// What a run of statements did. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct StreamOutcome { + pub rows: u64, + /// The reader stopped taking events, so the rest of the run was + /// skipped. + pub consumer_gone: bool, + /// A stop came between two statements, so the rest was not run. + pub halted: bool, +} + +/// The columns a statement returns, by name and by DuckDB type. +struct Shape { + names: Vec, + types: Vec, +} + +impl Shape { + fn of(statement: &duckdb::Statement<'_>) -> Self { + let schema = statement.schema(); + let names: Vec = schema.fields().iter().map(|field| field.name().clone()).collect(); + let types = (0..names.len()) + .map(|index| DuckType::from_logical(&statement.column_logical_type(index))) + .collect(); + Self { names, types } + } + + fn columns(&self) -> Arc<[ResultColumn]> { + self.names + .iter() + .zip(&self.types) + .map(|(name, duck)| ResultColumn::new(name.clone(), duck.column_type())) + .collect() + } + + fn rows(&self, chunk: &StructArray, from: usize, to: usize) -> Vec> { + (from..to) + .map(|row| { + chunk + .columns() + .iter() + .zip(&self.types) + .map(|(column, duck)| value_at(column.as_ref(), duck, row)) + .collect() + }) + .collect() + } +} + +/// Start `sql` with `params` on DuckDB's streaming result, giving back +/// the statement to read from and its columns. +fn start<'c>(link: &'c Link, sql: &str, params: &[BoundParam]) -> Result<(duckdb::Statement<'c>, Shape), DriverError> { + let values = bind_all(params)?; + let mut statement = link.connection.prepare(sql).map_err(map_error)?; + drop( + statement + .stream_arrow(duckdb::params_from_iter(values.iter())) + .map_err(map_error)?, + ); + let shape = Shape::of(&statement); + Ok((statement, shape)) +} + +fn next_chunk(statement: &duckdb::Statement<'_>) -> Result, DriverError> { + statement.step().map_err(map_error) +} + +/// A read the app built, kept to `limit` rows, and whether there were +/// more. +pub(crate) fn collect(link: &Link, statement: &Statement, limit: Option) -> Result { + let (running, shape) = start(link, statement.sql(), statement.params())?; + let keep = limit.map(|limit| usize::try_from(limit.get()).unwrap_or(usize::MAX)); + let mut rows = Vec::new(); + let mut truncated = false; + while let Some(chunk) = next_chunk(&running)? { + let room = keep.map_or(chunk.len(), |keep| keep.saturating_sub(rows.len())); + rows.extend(shape.rows(&chunk, 0, room.min(chunk.len()))); + if room < chunk.len() { + truncated = true; + break; + } + } + Ok(QueryResult::new(shape.columns(), rows).truncated(truncated)) +} + +/// The single number a count query returned. +pub(crate) fn count(link: &Link, statement: &Statement) -> Result { + let result = collect(link, statement, RowLimit::new(1))?; + match result.cell(0, 0) { + Some(value) => value + .as_exact_u64() + .ok_or_else(|| DriverError::Protocol(format!("a row count read as {value:?}"))), + None => Err(DriverError::Protocol("the count returned no row".to_owned())), + } +} + +/// Run a statement the app built for its effect, giving the number of +/// rows it changed. +pub(crate) fn execute(link: &Link, statement: &Statement) -> Result { + let values = bind_all(statement.params())?; + let mut prepared = link.connection.prepare(statement.sql()).map_err(map_error)?; + let changed = prepared + .execute(duckdb::params_from_iter(values.iter())) + .map_err(map_error)?; + u64::try_from(changed).map_err(|_| DriverError::Protocol("a row count past u64".to_owned())) +} + +/// Run plain SQL with no result to read, such as BEGIN or COMMIT. +pub(crate) fn batch(link: &Link, sql: &str) -> Result<(), DriverError> { + link.connection.execute_batch(sql).map_err(map_error) +} + +/// Where a run's events go: the async side reads them off the channel +/// and hands each to the sink before taking the next, so a sink that +/// falls behind holds the run here. +pub(crate) struct Events { + sender: mpsc::Sender, + gone: bool, +} + +impl Events { + pub(crate) fn new(sender: mpsc::Sender) -> Self { + Self { sender, gone: false } + } + + /// Hand one event over; false once nobody reads any more. + fn send(&mut self, event: ResultEvent) -> bool { + if !self.gone && self.sender.blocking_send(event).is_err() { + self.gone = true; + } + !self.gone + } +} + +/// Run SQL the user wrote, one statement after another, until one +/// fails, the reader goes or `halt` is raised. Each statement goes to +/// DuckDB alone, so each result set streams by itself. +pub(crate) fn run_script( + link: &Link, + sql: &str, + limit: Option, + events: &mut Events, + halt: &dyn Fn() -> bool, +) -> Result { + let mut outcome = StreamOutcome::default(); + for statement in split_statements(sql) { + if halt() { + outcome.halted = true; + break; + } + match attach_literal(statement) { + Some(literal) => { + let targets = attach_targets(link, literal)?; + attach_unless_open(&targets, || { + run_statement(link, statement, &[], limit, events, &mut outcome) + })?; + } + None => run_statement(link, statement, &[], limit, events, &mut outcome)?, + } + if outcome.consumer_gone { + break; + } + } + Ok(outcome) +} + +/// Run a read the app built, every row of it. +pub(crate) fn run_bound(link: &Link, statement: &Statement, events: &mut Events) -> Result { + let mut outcome = StreamOutcome::default(); + run_statement(link, statement.sql(), statement.params(), None, events, &mut outcome)?; + Ok(outcome) +} + +fn run_statement( + link: &Link, + sql: &str, + params: &[BoundParam], + limit: Option, + events: &mut Events, + outcome: &mut StreamOutcome, +) -> Result<(), DriverError> { + let (running, shape) = start(link, sql, params)?; + match command_result(sql, &shape) { + Some(Command::ChangedRows) => { + let changed = match next_chunk(&running)? { + Some(chunk) => shape.rows(&chunk, 0, chunk.len().min(1)).into_iter().next(), + None => None, + }; + while next_chunk(&running)?.is_some() {} + let rows_affected = changed.and_then(|row| row.first().and_then(Value::as_exact_u64)); + outcome.consumer_gone = !events.send(ResultEvent::CommandCompleted { rows_affected }); + Ok(()) + } + Some(Command::Nothing) => { + while next_chunk(&running)?.is_some() {} + outcome.consumer_gone = !events.send(ResultEvent::CommandCompleted { rows_affected: None }); + Ok(()) + } + None => stream_rows(&running, &shape, limit, events, outcome), + } +} + +/// A statement DuckDB answers with a status rather than rows. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Command { + /// One BIGINT `Count`: the rows an INSERT, UPDATE, DELETE, MERGE, + /// COPY or CREATE TABLE AS changed. A plain CREATE answers with the + /// same column and no row. + ChangedRows, + /// One BOOLEAN `Success` and no row: SET, ATTACH, DROP, a + /// transaction statement and the like. + Nothing, +} + +/// The words a statement that returns rows starts with. A statement of +/// any other kind returns rows only through RETURNING. What CALL and +/// EXECUTE return is the function's or the prepared statement's, which +/// may be a `Count` or `Success` column of its own, so it stays rows. +const QUERY_WORDS: [&str; 14] = [ + "SELECT", + "CALL", + "EXECUTE", + "VALUES", + "FROM", + "TABLE", + "SHOW", + "DESCRIBE", + "DESC", + "SUMMARIZE", + "EXPLAIN", + "PIVOT", + "UNPIVOT", + "PRAGMA", +]; + +/// What DuckDB's statement return type would say, read from the +/// columns it bound and the statement's words, since the duckdb crate +/// does not expose the return type itself. +fn command_result(sql: &str, shape: &Shape) -> Option { + let queries = main_verb(sql).is_none_or(|verb| QUERY_WORDS.contains(&verb.as_str())); + if queries || words(sql).iter().any(|word| word == "RETURNING") || shape.names.len() != 1 { + return None; + } + match (shape.names[0].as_str(), &shape.types[0]) { + ("Count", DuckType::Integer { .. }) => Some(Command::ChangedRows), + ("Success", DuckType::Boolean) => Some(Command::Nothing), + _ => None, + } +} + +fn stream_rows( + running: &duckdb::Statement<'_>, + shape: &Shape, + limit: Option, + events: &mut Events, + outcome: &mut StreamOutcome, +) -> Result<(), DriverError> { + if !events.send(ResultEvent::ResultSetStarted { + columns: shape.columns(), + }) { + outcome.consumer_gone = true; + return Ok(()); + } + let keep = limit.map_or(u64::MAX, RowLimit::get); + let mut retained = 0u64; + let mut truncation = Truncation::Complete; + 'chunks: while let Some(chunk) = next_chunk(running)? { + let mut from = 0; + while from < chunk.len() { + if retained >= keep { + truncation = Truncation::ReadPastLimit; + break 'chunks; + } + let room = usize::try_from(keep - retained).unwrap_or(usize::MAX); + let to = chunk.len().min(from + ROWS_PER_EVENT).min(from.saturating_add(room)); + let rows = shape.rows(&chunk, from, to); + retained += rows.len() as u64; + outcome.rows += rows.len() as u64; + if !events.send(ResultEvent::Rows(rows)) { + outcome.consumer_gone = true; + return Ok(()); + } + from = to; + } + } + let mut closing = Vec::new(); + if truncation == Truncation::ReadPastLimit { + closing.push(ResultEvent::RowLimitReached { retained }); + } + closing.push(ResultEvent::ResultSetFinished { + retained, + total_rows: (truncation == Truncation::Complete).then_some(retained), + truncation, + }); + for event in closing { + if !events.send(event) { + outcome.consumer_gone = true; + break; + } + } + Ok(()) +} + +/// Whether the statement changes rows and returns no rows of its own, +/// so its one value is the number it changed. +pub(crate) fn changes_rows(sql: &str) -> bool { + let changes = matches!( + main_verb(sql).as_deref(), + Some("INSERT" | "UPDATE" | "DELETE" | "MERGE" | "COPY" | "UPSERT") + ); + changes && !words(sql).iter().any(|word| word == "RETURNING") +} + +/// The statement's own verb, upper case: its first word, or past a WITH +/// clause the first word outside its parentheses that starts a +/// statement, as in `WITH src AS (...) INSERT INTO ...`. +fn main_verb(sql: &str) -> Option { + const VERBS: [&str; 15] = [ + "SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "VALUES", "FROM", "TABLE", "PIVOT", "UNPIVOT", + "SUMMARIZE", "DESCRIBE", "SHOW", "EXPLAIN", + ]; + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(sql, grammar, LexicalSettings::default_for(grammar)); + let mut depth = 0usize; + let mut code = plan.tokens(0..sql.len()).filter(|token| !token.kind.is_trivia()); + let first = code.next()?; + let word = |range: std::ops::Range| sql.get(range).map(str::to_ascii_uppercase); + let first_word = match first.kind { + ScriptTokenKind::Word => word(first.range)?, + ScriptTokenKind::OpenParen => return Some("SELECT".to_owned()), + _ => return None, + }; + if first_word != "WITH" { + return Some(first_word); + } + for token in code { + match token.kind { + ScriptTokenKind::OpenParen => depth += 1, + ScriptTokenKind::CloseParen => depth = depth.saturating_sub(1), + ScriptTokenKind::Word if depth == 0 => { + let candidate = word(token.range)?; + if VERBS.contains(&candidate.as_str()) { + return Some(candidate); + } + } + _ => {} + } + } + None +} + +/// Whether a statement of `sql` begins, ends or replaces a transaction. +/// A streamed read's own read-only transaction holds only while the +/// user's SQL leaves it alone. +pub(crate) fn controls_transaction(sql: &str) -> bool { + split_statements(sql).into_iter().any(|statement| { + words(statement).first().is_some_and(|first| { + matches!( + first.as_str(), + "BEGIN" | "START" | "COMMIT" | "END" | "ROLLBACK" | "ABORT" + ) + }) + }) +} + +/// The words an ATTACH may carry before its path. +const ATTACH_WORDS: [&str; 6] = ["DATABASE", "IF", "NOT", "EXISTS", "OR", "REPLACE"]; + +/// The source text of the path an ATTACH statement names: a string +/// constant in any of DuckDB's forms, which only an alias or the options +/// may follow. +fn attach_literal(statement: &str) -> Option<&str> { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(statement, grammar, LexicalSettings::default_for(grammar)); + let mut tokens = plan.tokens(0..statement.len()).filter(|token| !token.kind.is_trivia()); + let word = |token: &ScriptToken, words: &[&str]| { + token.kind == ScriptTokenKind::Word + && statement + .get(token.range.clone()) + .is_some_and(|text| words.iter().any(|word| text.eq_ignore_ascii_case(word))) + }; + let first = tokens.next()?; + if !word(&first, &["ATTACH"]) { + return None; + } + let start = tokens.find(|token| !word(token, &ATTACH_WORDS))?.range.start; + let end = tokens + .find(|token| token.kind == ScriptTokenKind::OpenParen || word(token, &["AS"])) + .map_or(statement.len(), |token| token.range.start); + statement.get(start..end).map(str::trim_end) +} + +/// The files an ATTACH may open. DuckDB decodes the path itself, so its +/// escapes and continued strings read as DuckDB reads them, then it is +/// expanded the way DuckDB opens it. Both readings of a +/// `name:` prefix are kept, since a TYPE option leaves it in the name. +/// +/// Text DuckDB cannot read as a string is no string constant, so DuckDB +/// refuses the ATTACH while parsing it and opens nothing. +fn attach_targets(link: &Link, literal: &str) -> Result, DriverError> { + // The path goes last, on a line of its own, so a comment after it + // hides nothing. + let read = link.connection.query_row( + &format!("SELECT current_setting('home_directory'),\n{literal}\n"), + [], + |row| Ok((row.get::<_, Option>(0)?, row.get::<_, duckdb::types::Value>(1)?)), + ); + let (home, path) = match read.map_err(map_error) { + Ok((home, duckdb::types::Value::Text(path))) => (home, path), + Ok(_) => return Ok(Vec::new()), + Err(error) if is_parse_error(&error) => return Ok(Vec::new()), + Err(error) => return Err(error), + }; + let home = home + .filter(|home| !home.is_empty()) + .map(OsString::from) + .or_else(|| std::env::var_os("HOME")); + let mut readings = vec![path.as_str()]; + if let Some(prefixed) = extension_prefix(&path) { + readings.push(path.get(prefixed.len() + 1..).unwrap_or_default()); + } + Ok(readings + .into_iter() + .filter(|path| !path.is_empty() && !path.starts_with(":memory:")) + .map(|path| expand_path(path, home.as_deref())) + .collect()) +} + +/// A `name:` before the path, which DuckDB reads as the engine that +/// opens it: at least two letters, digits or underscores, not a URL. +fn extension_prefix(path: &str) -> Option<&str> { + let colon = path.find(':').filter(|colon| *colon >= 2)?; + let prefix = path.get(..colon)?; + let is_url = path.get(colon..colon + 3) == Some("://"); + let is_name = prefix.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_'); + (is_name && !is_url).then_some(prefix) +} + +/// The path as DuckDB's file system opens it: a leading `~` is the home +/// directory, and a `file:` URL is the path it holds. +fn expand_path(path: &str, home: Option<&OsStr>) -> PathBuf { + if let Some(rest) = path.strip_prefix('~') { + let mut expanded = home.map(OsString::from).unwrap_or_default(); + expanded.push(rest); + return PathBuf::from(expanded); + } + let url_offset = match path.strip_prefix("file:/") { + None => 0, + Some(rest) if !rest.starts_with('/') => 5, + Some(rest) if rest.starts_with("//") => 7, + Some(rest) if rest.get(1..).is_some_and(|host| host.starts_with("localhost/")) => 16, + Some(_) => 0, + }; + PathBuf::from(path.get(url_offset..).unwrap_or(path)) +} + +/// The words of `sql`, upper case, past comments and outside strings +/// and quoted names. +fn words(sql: &str) -> Vec { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(sql, grammar, LexicalSettings::default_for(grammar)); + plan.tokens(0..sql.len()) + .filter(|token| token.kind == ScriptTokenKind::Word) + .filter_map(|token| sql.get(token.range)) + .map(str::to_ascii_uppercase) + .collect() +} + +/// The statements of a script, split where DuckDB's parser ends one. +/// +/// An unterminated string or identifier runs to the end of the text, +/// and the statement it opens goes to DuckDB as it stands, so the error +/// is DuckDB's own. An open comment is a comment, so only the text +/// before it is sent. +pub(crate) fn split_statements(sql: &str) -> Vec<&str> { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(sql, grammar, LexicalSettings::default_for(grammar)); + let open = plan + .diagnostics() + .iter() + .map(|ScriptDiagnostic::Unterminated { construct, start }| (*start, *construct)) + .min_by_key(|(start, _)| *start); + let (open_at, tail_end) = match open { + Some((start, OpenConstruct::BlockComment)) => (start, start), + Some((start, _)) => (start, sql.len()), + None => (sql.len(), sql.len()), + }; + let mut statements = Vec::with_capacity(plan.statements().len()); + for statement in plan.statements() { + if statement.range.end <= open_at { + statements.push(statement.text(sql)); + continue; + } + let tail = sql.get(statement.range.start..tail_end).unwrap_or_default().trim_end(); + if !tail.is_empty() { + statements.push(tail); + } + break; + } + statements +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn statements_split_as_duckdb_parses_them() { + assert_eq!( + split_statements("SELECT 1; SELECT $$a;b$$; /* c; */ SELECT 'd;e'"), + ["SELECT 1", "SELECT $$a;b$$", "SELECT 'd;e'"] + ); + assert_eq!(split_statements("SELECT 1; SELECT 'open"), ["SELECT 1", "SELECT 'open"]); + assert_eq!(split_statements("SELECT 1; /* open"), ["SELECT 1"]); + assert!(split_statements(" -- only a comment\n").is_empty()); + } + + #[test] + fn only_a_row_change_without_returning_reports_a_count() { + assert!(changes_rows("INSERT INTO t VALUES (1)")); + assert!(changes_rows("/* why */ update t SET a = 1")); + assert!(changes_rows("COPY t FROM 'x.csv'")); + assert!(!changes_rows("DELETE FROM t RETURNING *")); + assert!(!changes_rows("SELECT 1 AS \"Count\"")); + assert!(!changes_rows("CREATE TABLE t (a INTEGER)")); + assert!(changes_rows("INSERT INTO t VALUES ('returning')")); + assert!(changes_rows("WITH src AS (SELECT 1 AS a) INSERT INTO t SELECT a FROM src")); + assert!(!changes_rows("WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x")); + assert_eq!(main_verb("WITH RECURSIVE r AS (SELECT 1) SELECT * FROM r").as_deref(), Some("SELECT")); + assert_eq!(main_verb("(SELECT 1)").as_deref(), Some("SELECT")); + } + + #[test] + fn an_attach_names_its_path_with_a_string_constant() { + assert_eq!(attach_literal("ATTACH $db$/srv/b.db$db$ AS b"), Some("$db$/srv/b.db$db$")); + assert_eq!(attach_literal(r"ATTACH E'\x2fsrv/c.db'"), Some(r"E'\x2fsrv/c.db'")); + assert_eq!( + attach_literal("/* x */ attach database if not exists 'it''s.db' (READ_ONLY)"), + Some("'it''s.db'") + ); + assert_eq!(attach_literal("ATTACH OR REPLACE U&'d\\0061ta.db' AS d"), Some("U&'d\\0061ta.db'")); + assert_eq!(attach_literal("ATTACH '/srv/'\n-- more\n'a.db' AS a"), Some("'/srv/'\n-- more\n'a.db'")); + assert_eq!(attach_literal("SELECT 'ATTACH x'"), None); + assert_eq!(attach_literal("DETACH s"), None); + } + + #[test] + fn a_path_is_expanded_as_duckdb_opens_it() { + let home = Some(OsStr::new("/home/ana")); + + assert_eq!(expand_path("~/a.db", home), PathBuf::from("/home/ana/a.db")); + assert_eq!(expand_path("~a.db", home), PathBuf::from("/home/anaa.db")); + assert_eq!(expand_path("file:///srv/a.db", home), PathBuf::from("/srv/a.db")); + assert_eq!(expand_path("file://localhost/srv/a.db", home), PathBuf::from("/srv/a.db")); + assert_eq!(expand_path("file:/srv/a.db", home), PathBuf::from("/srv/a.db")); + assert_eq!(expand_path("file://host/srv/a.db", home), PathBuf::from("file://host/srv/a.db")); + assert_eq!(expand_path("rel/a.db", home), PathBuf::from("rel/a.db")); + } + + #[test] + fn an_engine_prefix_is_a_name_before_a_colon() { + assert_eq!(extension_prefix("duckdb:/srv/a.db"), Some("duckdb")); + assert_eq!(extension_prefix("sqlite:a.db"), Some("sqlite")); + assert_eq!(extension_prefix("s3://bucket/a.db"), None); + assert_eq!(extension_prefix("c:/a.db"), None); + assert_eq!(extension_prefix("my file:a.db"), None); + assert_eq!(extension_prefix("/srv/a.db"), None); + } + + #[test] + fn what_a_call_or_an_execute_returns_stays_rows() { + let count = Shape { + names: vec!["Count".to_owned()], + types: vec![DuckType::Integer { + width: tablepro_core::column::IntegerWidth::W64, + signed: true, + }], + }; + + assert_eq!(command_result("EXECUTE q", &count), None); + assert_eq!(command_result("CALL counted()", &count), None); + assert_eq!(command_result("INSERT INTO t VALUES (1)", &count), Some(Command::ChangedRows)); + } + + #[test] + fn transaction_control_is_found_in_any_statement() { + assert!(controls_transaction("SELECT 1; COMMIT; DELETE FROM t")); + assert!(controls_transaction("/* x */ begin transaction")); + assert!(controls_transaction("SELECT 1; ABORT")); + assert!(!controls_transaction("SELECT 'COMMIT'; SELECT \"end\" FROM t")); + assert!(!controls_transaction("WITH x AS (SELECT 1) SELECT * FROM x")); + } +} diff --git a/linux/crates/drivers/duckdb/src/stop.rs b/linux/crates/drivers/duckdb/src/stop.rs new file mode 100644 index 0000000000..878db2bd26 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/stop.rs @@ -0,0 +1,70 @@ +//! Stopping a statement DuckDB is running. +//! +//! `duckdb_interrupt` sets a flag on the connection that the statement +//! running there checks as it works, and the statement fails with an +//! interrupt error. DuckDB clears the flag as each statement starts, so +//! a stop sent while the work is between statements would be lost: the +//! stop is sent again until the work returns or holds stops off for its +//! cleanup. The flag names the connection, not the statement, so the +//! connection of a call it may have missed is not reused. + +use std::sync::{Mutex, PoisonError}; +use std::time::Duration; + +use async_trait::async_trait; +use tablepro_core::{DriverError, EngineStop, StopDelivery, StopScope}; + +use crate::errors::is_interrupt; +use crate::instance::StopHandle; + +/// How often a stop is sent again while the work it is for still runs. +const RESEND: Duration = Duration::from_millis(10); + +/// One call's stop. It is armed once the call holds a connection; a +/// stop asked for before that finds nothing to interrupt, and the call +/// sees the request before it sends anything. +#[derive(Default)] +pub(crate) struct DuckStop { + armed: Mutex>, +} + +impl DuckStop { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn arm(&self, handle: Option) { + *self.armed.lock().unwrap_or_else(PoisonError::into_inner) = handle; + } + + fn armed(&self) -> Option { + self.armed.lock().unwrap_or_else(PoisonError::into_inner).clone() + } +} + +/// Interrupt the connection until the work on it no longer takes stops. +pub(crate) async fn interrupt_until_settled(handle: &StopHandle) { + while handle.is_stoppable() { + handle.interrupt(); + tokio::time::sleep(RESEND).await; + } +} + +#[async_trait] +impl EngineStop for DuckStop { + fn delivery(&self) -> StopDelivery { + StopDelivery::OutOfBand(StopScope::Connection) + } + + /// Settles once the work has given way or holds stops off. + async fn stop(&self) -> Result<(), DriverError> { + if let Some(handle) = self.armed() { + interrupt_until_settled(&handle).await; + } + Ok(()) + } + + fn acknowledged(&self, error: &DriverError) -> bool { + is_interrupt(error) + } +} diff --git a/linux/crates/drivers/duckdb/src/transaction_state.rs b/linux/crates/drivers/duckdb/src/transaction_state.rs new file mode 100644 index 0000000000..665d6c1e3d --- /dev/null +++ b/linux/crates/drivers/duckdb/src/transaction_state.rs @@ -0,0 +1,31 @@ +//! Where a connection stands with a transaction. +//! +//! DuckDB's C API has no autocommit flag to read. A transaction's id +//! stays the same from one statement to the next only inside an +//! explicit transaction, since outside one each statement runs in its +//! own, and a transaction a statement failed in refuses every statement +//! until it is rolled back. + +use tablepro_core::{DriverError, TransactionState}; + +use crate::errors::map_error; +use crate::instance::Link; + +pub(crate) fn transaction_state(link: &Link) -> TransactionState { + match (transaction_id(link), transaction_id(link)) { + (Ok(first), Ok(second)) if first == second => TransactionState::InTransaction, + (Ok(_), Ok(_)) => TransactionState::Idle, + (Err(error), _) | (_, Err(error)) if is_aborted(&error) => TransactionState::Failed, + _ => TransactionState::Unknown, + } +} + +fn transaction_id(link: &Link) -> Result { + link.connection + .query_row("SELECT txid_current()", [], |row| row.get::<_, u64>(0)) + .map_err(map_error) +} + +fn is_aborted(error: &DriverError) -> bool { + matches!(error, DriverError::Server(diagnostics) if diagnostics.message.contains("transaction is aborted")) +} diff --git a/linux/crates/drivers/duckdb/src/write_batch.rs b/linux/crates/drivers/duckdb/src/write_batch.rs new file mode 100644 index 0000000000..7002651726 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/write_batch.rs @@ -0,0 +1,148 @@ +//! A save, as one DuckDB transaction. +//! +//! DuckDB checks conflicts with other connections at COMMIT, and a +//! statement that fails ends the transaction's usefulness: everything +//! after it is refused until it is rolled back. So the batch stops at +//! the first failure and rolls back; a COMMIT DuckDB refuses has rolled +//! back too. + +use tablepro_core::statement::{GuardedStatement, RowGuard}; +use tablepro_core::{ + DriverError, LossPhase, SessionFate, StopReason, StopRequest, TimeoutPhase, TransactionState, WriteBatch, + WriteLedger, WriteReport, +}; + +use crate::errors::is_interrupt; +use crate::instance::Link; +use crate::result_stream::{batch, changes_rows, count, execute}; +use crate::transaction_state::transaction_state; + +/// Run `write` on the held connection, recording each step in `ledger`, +/// and say whether the connection is still fit for the pool: one whose +/// rollback failed may hold the transaction still. +pub(crate) fn run( + link: &Link, + write: &WriteBatch, + ledger: &WriteLedger, + request: &StopRequest, +) -> (Result, bool) { + let result = transaction(link, write, ledger, request); + let reusable = transaction_state(link) == TransactionState::Idle; + (result, reusable) +} + +fn transaction( + link: &Link, + write: &WriteBatch, + ledger: &WriteLedger, + request: &StopRequest, +) -> Result { + if let Err(error) = batch(link, "BEGIN TRANSACTION") { + return Err(match stopped(&error, request) { + Some(cause) => ledger.interrupted(cause, SessionFate::RolledBack), + None => error, + }); + } + let mut rows_affected = Vec::with_capacity(write.steps.len()); + for (index, step) in write.steps.iter().enumerate() { + if let Some(cause) = request.reason().map(stop_cause) { + return Err(ledger.interrupted(cause, rollback(link))); + } + match run_step(link, ledger, index, step) { + Ok(affected) => rows_affected.push(affected), + Err(StepFailure::Guard { expected, actual }) => { + return Err(ledger.guard_failed(index, expected, actual, rollback(link))); + } + Err(StepFailure::Error(error)) => { + let fate = rollback(link); + return Err(match stopped(&error, request) { + Some(cause) => ledger.interrupted(cause, fate), + None => ledger.failed(index, error, fate), + }); + } + } + } + if let Some(cause) = request.reason().map(stop_cause) { + return Err(ledger.interrupted(cause, rollback(link))); + } + link.hold_stops(); + ledger.commit_sending(None); + match batch(link, "COMMIT") { + Ok(()) => Ok(WriteReport::new(rows_affected)), + // A COMMIT DuckDB refuses, a conflict with another connection's + // write among them, rolled the transaction back. + Err(error) => { + let _ = batch(link, "ROLLBACK"); + Err(ledger.failed(write.steps.len(), error, SessionFate::RolledBack)) + } + } +} + +enum StepFailure { + Guard { expected: u64, actual: u64 }, + Error(DriverError), +} + +/// Probe, send and check one step. +fn run_step( + link: &Link, + ledger: &WriteLedger, + index: usize, + step: &GuardedStatement, +) -> Result, StepFailure> { + if let Some((probe, expected)) = step.guard.probe() { + let actual = count(link, probe).map_err(StepFailure::Error)?; + if actual != expected { + return Err(StepFailure::Guard { expected, actual }); + } + } + ledger.statement_sending(index); + let affected = execute(link, &step.statement).map_err(StepFailure::Error)?; + ledger.statement_confirmed(index); + if let RowGuard::AffectedExactly(expected) = step.guard + && affected != expected + { + return Err(StepFailure::Guard { + expected, + actual: affected, + }); + } + Ok(changes_rows(step.statement.sql()).then_some(affected)) +} + +/// End the transaction. A transaction DuckDB already ended leaves +/// nothing to roll back, which is the same answer. +fn rollback(link: &Link) -> SessionFate { + link.hold_stops(); + match batch(link, "ROLLBACK") { + Ok(()) => SessionFate::RolledBack, + Err(DriverError::Server(diagnostics)) if diagnostics.message.contains("no transaction is active") => { + SessionFate::RolledBack + } + Err(error) => { + tracing::warn!(%error, "a rollback failed"); + SessionFate::Discarded + } + } +} + +/// What the caller is told when `error` is DuckDB giving way to a stop. +fn stopped(error: &DriverError, request: &StopRequest) -> Option { + match is_interrupt(error) { + true => request.reason().map(stop_cause), + false => None, + } +} + +pub(crate) fn stop_cause(reason: StopReason) -> DriverError { + match reason { + StopReason::Cancelled => DriverError::Cancelled, + StopReason::DeadlineElapsed => DriverError::Timeout { + phase: TimeoutPhase::Statement, + server_cancelled: true, + }, + StopReason::Shutdown => DriverError::ConnectionLost { + during: LossPhase::Statement, + }, + } +} diff --git a/linux/crates/drivers/duckdb/tests/integration/catalog.rs b/linux/crates/drivers/duckdb/tests/integration/catalog.rs new file mode 100644 index 0000000000..f4f59e7a2b --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/catalog.rs @@ -0,0 +1,250 @@ +//! What the catalogue reads report for a DuckDB file. + +use tablepro_core::column::ColumnDefault; +use tablepro_core::meta::{IndexKey, ObjectKind, ObjectScope, RowCountEstimate, RowIdentity, TableRef}; +use tablepro_core::{RunEnd, Value}; + +use crate::support::{TestFile, apply, call, query, run, session}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn schemas_and_objects_are_the_files_own() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE SCHEMA sales", + "CREATE TABLE sales.orders (id INTEGER)", + "CREATE TABLE people (id INTEGER)", + "CREATE VIEW people_view AS SELECT * FROM people", + ], + ) + .await + .unwrap(); + let editor = session(connection.as_ref()).await; + let other = file.path.with_file_name("other.duckdb"); + let (attached, _) = run( + editor.as_ref(), + &format!( + "ATTACH '{}' AS other; CREATE TABLE other.elsewhere (a INTEGER)", + other.display() + ), + ) + .await; + assert_eq!(attached.end, RunEnd::Completed); + + let schemas = connection.list_schemas(call()).await.unwrap(); + let names: Vec<(&str, bool)> = schemas + .iter() + .map(|schema| (schema.name.as_str(), schema.is_default)) + .collect(); + let main = connection + .list_objects(&ObjectScope::DefaultSchema, call()) + .await + .unwrap(); + let all = connection.list_objects(&ObjectScope::AllSchemas, call()).await.unwrap(); + + assert_eq!(names, [("main", true), ("sales", false)]); + let listed: Vec<(&str, ObjectKind)> = main.iter().map(|object| (object.name.as_str(), object.kind)).collect(); + assert_eq!( + listed, + [("people", ObjectKind::Table), ("people_view", ObjectKind::View)] + ); + assert!( + all.iter() + .any(|object| object.name == "orders" && object.schema.as_deref() == Some("sales")) + ); + assert!( + !all.iter().any(|object| object.name == "elsewhere"), + "an attached database listed as this file's" + ); + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn columns_carry_their_key_defaults_and_generated_expression() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE SEQUENCE numbers", + "CREATE TABLE t (id BIGINT DEFAULT nextval('numbers') PRIMARY KEY, label VARCHAR NOT NULL DEFAULT 'it''s', \ + made TIMESTAMP DEFAULT current_timestamp, doubled BIGINT GENERATED ALWAYS AS (id * 2) VIRTUAL)", + "COMMENT ON COLUMN t.label IS 'what it is'", + ], + ) + .await + .unwrap(); + + let columns = connection + .fetch_columns(&TableRef::unqualified("t"), call()) + .await + .unwrap(); + + let id = &columns[0]; + assert!(id.primary_key && id.is_auto_increment && !id.nullable); + let label = &columns[1]; + assert_eq!(label.default, ColumnDefault::Literal(Value::Text("it's".to_owned()))); + assert_eq!(label.comment.as_deref(), Some("what it is")); + assert!(!label.nullable); + assert!( + matches!(&columns[2].default, ColumnDefault::Expression(expression) if expression.as_sql() == "current_timestamp") + ); + let doubled = &columns[3]; + assert!( + doubled.is_generated && doubled.default == ColumnDefault::None, + "{doubled:?}" + ); + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn keys_and_indexes_are_listed_with_the_keys_that_point_here() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE parent (id INTEGER PRIMARY KEY, code VARCHAR UNIQUE, note VARCHAR)", + "CREATE TABLE child (id INTEGER, parent_id INTEGER REFERENCES parent (id))", + "CREATE INDEX parent_note ON parent (note)", + "CREATE INDEX parent_lower ON parent ((lower(note)))", + ], + ) + .await + .unwrap(); + let parent = TableRef::unqualified("parent"); + + let indexes = connection.fetch_indexes(&parent, call()).await.unwrap(); + let keys = connection + .fetch_foreign_keys(&TableRef::unqualified("child"), call()) + .await + .unwrap(); + let definition = connection.fetch_table_definition(&parent, call()).await.unwrap(); + + let primary = indexes.iter().find(|index| index.primary).unwrap(); + assert_eq!(primary.key_columns(), ["id"]); + assert!( + indexes + .iter() + .any(|index| index.unique && !index.primary && index.key_columns() == ["code"]) + ); + assert!(indexes.iter().any(|index| index.name == "parent_note" && !index.unique)); + let expression = indexes.iter().find(|index| index.name == "parent_lower").unwrap(); + assert!( + matches!(&expression.keys[0], IndexKey::Expression { .. }), + "{expression:?}" + ); + assert_eq!(keys.len(), 1); + assert_eq!( + (keys[0].ref_table.as_str(), keys[0].columns.clone()), + ("parent", vec!["parent_id".to_owned()]) + ); + assert_eq!(definition.inbound_foreign_keys.len(), 1); + assert_eq!(definition.inbound_foreign_keys[0].table.name, "child"); + let source = definition.source.unwrap(); + assert!(source.create_statement.starts_with("CREATE TABLE parent")); + assert_eq!(source.dependents.len(), 2, "{:?}", source.dependents); + connection.close().await.unwrap(); +} + +/// A row is named by its key, or by a UNIQUE key over NOT NULL columns. +/// Without one the grid is read-only: DuckDB's rowid names no row. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_row_is_named_by_a_key_and_never_by_rowid() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE keyed (id INTEGER PRIMARY KEY)", + "CREATE TABLE coded (code VARCHAR NOT NULL UNIQUE, other VARCHAR UNIQUE)", + "CREATE TABLE loose (a INTEGER UNIQUE)", + ], + ) + .await + .unwrap(); + let mut identities = Vec::new(); + for name in ["keyed", "coded", "loose"] { + identities.push( + connection + .row_identity(&TableRef::unqualified(name), call()) + .await + .unwrap(), + ); + } + + assert_eq!( + identities, + [ + RowIdentity::UniqueKey { + columns: vec!["id".to_owned()] + }, + RowIdentity::UniqueKey { + columns: vec!["code".to_owned()] + }, + RowIdentity::Unordered, + ] + ); + connection.close().await.unwrap(); +} + +/// Why a table without a key is read-only: a checkpoint that compacts +/// row groups hands the rows new rowids. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_rowid_moves_when_a_checkpoint_compacts_the_table() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE k (a INTEGER)", + "INSERT INTO k SELECT range FROM range(400000)", + ], + ) + .await + .unwrap(); + let editor = session(connection.as_ref()).await; + run(editor.as_ref(), "CHECKPOINT").await; + let before = query(connection.as_ref(), "SELECT rowid FROM k WHERE a = 390000").await; + + run( + editor.as_ref(), + "DELETE FROM k WHERE a < 240000 AND a % 100 <> 0; CHECKPOINT", + ) + .await; + let after = query(connection.as_ref(), "SELECT rowid FROM k WHERE a = 390000").await; + + assert_eq!(before.rows, vec![vec![Value::Int(390_000)]]); + assert_ne!( + after.rows, before.rows, + "the rowid stayed, so it may name a row after all" + ); + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn the_estimate_is_the_storage_count() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE t (a INTEGER)", + "INSERT INTO t SELECT range FROM range(1234)", + ], + ) + .await + .unwrap(); + + let estimate = connection + .table_estimate(&TableRef::unqualified("t"), call()) + .await + .unwrap(); + + assert_eq!(estimate.rows, RowCountEstimate::Approximate(1234)); + connection.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/conformance_engine.rs b/linux/crates/drivers/duckdb/tests/integration/conformance_engine.rs new file mode 100644 index 0000000000..87b459e698 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/conformance_engine.rs @@ -0,0 +1,19 @@ +//! The connection and DDL conformance suites on DuckDB. + +use crate::duckdb_engine::DuckDbEngine; + +tablepro_test_support::ddl_conformance_suite! { + engine: DuckDbEngine, + docker: false, +} + +// DuckDB has no server statement timeout, so no case bounds DDL by one. +tablepro_test_support::connection_conformance_suite! { + engine: DuckDbEngine, + docker: false, + endpoint: file, + pool: pooled, + editor_transactions: true, + unsupported_types: true, + bounded_ddl: false, +} diff --git a/linux/crates/drivers/duckdb/tests/integration/ddl.rs b/linux/crates/drivers/duckdb/tests/integration/ddl.rs new file mode 100644 index 0000000000..6eeee398fe --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/ddl.rs @@ -0,0 +1,237 @@ +//! Structure changes DuckDB takes and refuses, beyond the conformance +//! suite: what something depending on a table holds in place. + +use tablepro_core::ddl::{BuildDdlError, DdlOperation, DraftColumn, DraftType, RefusalReason, TableDraft, diff_table}; +use tablepro_core::meta::TableRef; +use tablepro_core::{Connection, DriverError, Value}; + +use crate::support::{TestFile, apply, call, query}; + +async fn definition(connection: &dyn Connection, name: &str) -> tablepro_core::ddl::TableDefinition { + connection + .fetch_table_definition(&TableRef::unqualified(name), call()) + .await + .unwrap() +} + +/// A table an index is made on takes no rename and no change to a +/// column but its default, and the plan says why before DuckDB would. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_index_holds_its_table_in_place() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &["CREATE TABLE t (a INTEGER, b VARCHAR)", "CREATE INDEX t_b ON t (b)"], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + + let mut renamed = TableDraft::from_definition(&original); + renamed.columns[0].name = "aa".to_owned(); + let refused = ddl.plan_alter_table(&original, &diff_table(&original, &renamed)); + let direct = apply(connection.as_ref(), &["ALTER TABLE t RENAME COLUMN a TO aa"]).await; + + assert!( + matches!( + &refused, + Err(BuildDdlError::Refused { + operation: DdlOperation::RenameColumn, + reason: RefusalReason::ColumnInUse(users), + .. + }) if users == &["t_b".to_owned()] + ), + "{refused:?}" + ); + assert!( + matches!(direct, Err(DriverError::RolledBack { .. })), + "DuckDB took the rename the plan refuses: {direct:?}" + ); + assert!(!ddl.table_capabilities(&original).rename_table); + + let mut defaulted = TableDraft::from_definition(&original); + defaulted.columns[0].default = tablepro_core::column::ColumnDefault::Literal(Value::Int(3)); + let plan = ddl + .plan_alter_table(&original, &diff_table(&original, &defaulted)) + .unwrap(); + connection + .write_batch(&plan.into_write_batch(TableRef::unqualified("t")), call()) + .await + .unwrap(); + connection.close().await.unwrap(); +} + +/// DuckDB adds no column with a constraint, so a NOT NULL column is +/// added and then made NOT NULL. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_required_column_is_added_then_made_required() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &["CREATE TABLE t (a INTEGER)", "INSERT INTO t VALUES (1)"], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + let mut draft = TableDraft::from_definition(&original); + let mut column = DraftColumn::new("b", DraftType::Typed("INTEGER".to_owned())); + column.nullable = false; + column.default = tablepro_core::column::ColumnDefault::Literal(Value::Int(5)); + draft.columns.push(column); + + let plan = ddl.plan_alter_table(&original, &diff_table(&original, &draft)).unwrap(); + connection + .write_batch(&plan.into_write_batch(TableRef::unqualified("t")), call()) + .await + .unwrap(); + + let after = definition(connection.as_ref(), "t").await; + let added = after.column("b").unwrap(); + assert!(!added.info.nullable); + assert_eq!( + query(connection.as_ref(), "SELECT b FROM t").await.rows, + vec![vec![Value::Int(5)]] + ); + connection.close().await.unwrap(); +} + +/// A key goes on a table that has none; one already there stays. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_key_is_added_but_never_moved() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE loose (a INTEGER NOT NULL)", + "CREATE TABLE keyed (a INTEGER PRIMARY KEY, b INTEGER)", + ], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + + let loose = definition(connection.as_ref(), "loose").await; + let mut keyed_draft = TableDraft::from_definition(&loose); + keyed_draft.columns[0].primary_key = true; + let plan = ddl.plan_alter_table(&loose, &diff_table(&loose, &keyed_draft)).unwrap(); + connection + .write_batch(&plan.into_write_batch(TableRef::unqualified("loose")), call()) + .await + .unwrap(); + assert!(definition(connection.as_ref(), "loose").await.primary_key.is_some()); + + let keyed = definition(connection.as_ref(), "keyed").await; + let mut moved = TableDraft::from_definition(&keyed); + moved.columns[0].primary_key = false; + moved.columns[1].primary_key = true; + assert!(ddl.plan_alter_table(&keyed, &diff_table(&keyed, &moved)).is_err()); + assert!(!ddl.table_capabilities(&keyed).alter_primary_key); + connection.close().await.unwrap(); +} + +/// A UNIQUE key's index is listed with the table's indexes, and only an +/// index made with CREATE INDEX can be dropped: DuckDB drops no +/// constraint. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_unique_key_index_is_not_dropped_on_its_own() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE t (a INTEGER UNIQUE, b INTEGER)", + "CREATE INDEX t_b ON t (b)", + ], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + let unique = original.indexes.iter().find(|index| index.unique).unwrap().clone(); + let created = original + .indexes + .iter() + .find(|index| index.name == "t_b") + .unwrap() + .clone(); + + let mut without_key = TableDraft::from_definition(&original); + without_key.indexes.retain(|index| index.name != unique.name); + let refused = ddl.plan_alter_table(&original, &diff_table(&original, &without_key)); + let direct = apply(connection.as_ref(), &[&format!("DROP INDEX \"{}\"", unique.name)]).await; + + assert!(!ddl.index_removal(&original, &unique).is_allowed()); + assert!(ddl.index_removal(&original, &created).is_allowed()); + assert_eq!(refused.err(), Some(BuildDdlError::Unsupported(DdlOperation::DropIndex))); + assert!(direct.is_err(), "DuckDB dropped the index of a UNIQUE key"); + connection.close().await.unwrap(); +} + +/// DuckDB refuses a new type for any column a CHECK names, the CHECK +/// over two columns included. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_check_over_two_columns_holds_both_types() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &["CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER, CHECK (a < b))"], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + + let allowed = |name: &str| { + ddl.column_capabilities(&original, original.column(name).unwrap()) + .alter_type + .is_allowed() + }; + let direct = apply(connection.as_ref(), &["ALTER TABLE t ALTER b TYPE BIGINT"]).await; + + assert!(!allowed("a") && !allowed("b")); + assert!(allowed("c")); + assert!(direct.is_err(), "DuckDB changed the type of a column a CHECK names"); + connection.close().await.unwrap(); +} + +/// A CHECK on one column goes with it when it is dropped. DuckDB drops +/// no column a CHECK shares with another. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_column_a_check_shares_is_not_dropped() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &["CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER CHECK (c > 0), CHECK (a < b))"], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + + let droppable = |name: &str| { + ddl.column_capabilities(&original, original.column(name).unwrap()) + .drop + .is_allowed() + }; + let shared = apply(connection.as_ref(), &["ALTER TABLE t DROP COLUMN a"]).await; + let alone = apply(connection.as_ref(), &["ALTER TABLE t DROP COLUMN c"]).await; + + assert!(!droppable("a") && !droppable("b")); + assert!(droppable("c")); + assert!(shared.is_err(), "DuckDB dropped a column a CHECK shares"); + assert!(alone.is_ok(), "{alone:?}"); + connection.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/decode.rs b/linux/crates/drivers/duckdb/tests/integration/decode.rs new file mode 100644 index 0000000000..27a372b1e3 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/decode.rs @@ -0,0 +1,124 @@ +//! Values read off DuckDB's results, checked against DuckDB's own text +//! for them. + +use tablepro_core::RunEnd; +use tablepro_core::value::{OtherValue, Value}; + +use crate::support::{TestFile, run, session}; + +/// Nested values an editor run reads come back as DuckDB's own text, +/// the text a table tab gets from `CAST(... AS VARCHAR)`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn nested_values_read_as_duckdb_prints_them() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let values = [ + "[1, 2, NULL]", + "['a', 'b c', '', 'it''s', 'x,y', ' lead', 'trail ', 'null', 'a\\b', '[x]', '{y}', 'k=v', 'a:b']", + "[[1, 2], [3], []]", + "[1.0, 0.1, 1e300, 1e-7, -2.5]::DOUBLE[]", + "[1.5, 0.1]::FLOAT[]", + "[1.5, -0.005]::DECIMAL(10,3)[]", + "[TIMESTAMP '2024-01-01 10:00:00', TIMESTAMP '2024-01-01 10:00:00.123', NULL]", + "[TIMESTAMP_NS '2024-01-01 00:00:00.123456789']", + "[DATE '2024-02-29', DATE '0044-03-15 (BC)', 'infinity'::DATE]", + "[TIME '12:00:00', TIME '23:59:59.5']", + "[INTERVAL '1 year 2 months 3 days 04:05:06.789', INTERVAL '-1 day', INTERVAL '0 seconds']", + "['\\xAA'::BLOB, 'abc'::BLOB, 'a''b\\x5Cc'::BLOB]", + "[true, false]", + "[170141183460469231731687303715884105727::HUGEINT]", + "['a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID]", + "['10101'::BIT]", + "[TIMESTAMPTZ '2024-03-10 10:30:00+02']", + "{'a': 1, 'b': 'x y', 'c': NULL, 'd': 'it''s', 'e f': [1, 2], 'g': {'h': 'i'}}", + "MAP {'k1': 1, 'k 2': NULL, 'a,b': 3}", + "MAP {1: [1, 2], 2: []}", + "[1, 2, 3]::INTEGER[3]", + "union_value(n := 2)::UNION(n INTEGER, s VARCHAR)", + "union_value(s := 'x, y')::UNION(n INTEGER, s VARCHAR)", + "[union_value(s := 'x, y')::UNION(n INTEGER, s VARCHAR)]", + "[{'k': 'a,b'}, NULL]", + "['happy'::ENUM('sad', 'happy')]", + ]; + let mut wrong = Vec::new(); + for value in values { + let (summary, collected) = run( + editor.as_ref(), + &format!("SELECT {value} AS v, CAST({value} AS VARCHAR) AS t"), + ) + .await; + assert_eq!(summary.end, RunEnd::Completed, "{value}"); + let rows = collected.rows(); + let (read, text) = (&rows[0][0], &rows[0][1]); + let Value::Text(expected) = text else { + panic!("the cast of {value} read as {text:?}"); + }; + match read { + Value::Other(other) if other.text == *expected => {} + other => wrong.push(format!("{value}: read {other:?}, DuckDB prints {expected:?}")), + } + } + assert!(wrong.is_empty(), "{}", wrong.join("\n")); + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_time_with_an_offset_is_not_read_without_it() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let (_, collected) = run( + editor.as_ref(), + "SELECT TIMETZ '12:30:00+05:30' AS t, [TIMETZ '01:00:00+00'] AS l", + ) + .await; + + for value in &collected.rows()[0] { + assert!(matches!(value, Value::Undecodable(_)), "{value:?}"); + } + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_nested_value_carries_its_type_name() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let (_, collected) = run(editor.as_ref(), "SELECT {'a': 1, 'b c': ['x']} AS v").await; + + assert_eq!( + collected.rows()[0][0], + Value::Other(Box::new(OtherValue { + type_name: "STRUCT(a INTEGER, \"b c\" VARCHAR[])".to_owned(), + text: "{'a': 1, 'b c': [x]}".to_owned(), + })) + ); + editor.close().await; + connection.close().await.unwrap(); +} + +/// DuckDB's Arrow output wraps an interval past about 292 years, which +/// reads as unreadable rather than as another interval. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_interval_arrow_cannot_carry_is_not_read_as_another() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let (_, collected) = run( + editor.as_ref(), + "SELECT INTERVAL '10000000000000000 microseconds' AS long, INTERVAL '1 day' AS short", + ) + .await; + + let rows = collected.rows(); + assert!(matches!(&rows[0][0], Value::Undecodable(_)), "{:?}", rows[0][0]); + assert!(matches!(&rows[0][1], Value::Interval(_)), "{:?}", rows[0][1]); + editor.close().await; + connection.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/duckdb_engine.rs b/linux/crates/drivers/duckdb/tests/integration/duckdb_engine.rs new file mode 100644 index 0000000000..b41dd79ca9 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/duckdb_engine.rs @@ -0,0 +1,144 @@ +//! The conformance engine adapter on DuckDB. The `large_table` test +//! target shares it, so it uses nothing else from this target. + +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use tablepro_core::{ + ConnectEnvironment, ConnectOptions, ConnectTarget, DatabaseDriver, FileEndpoint, LivenessPolicy, TableRef, + WorkspaceSettings, +}; +use tablepro_driver_duckdb::DuckDbDriver; +use tablepro_test_support::conformance::{ + ConformanceDdlEngine, ConformanceEngine, ConformanceFailure, EditorTransactionEngine, LargeTableEngine, + PooledConformanceEngine, TypedConformanceEngine, UnsupportedTypeCase, +}; +use tempfile::TempDir; + +/// Make an empty database the way another program would, so the driver +/// only ever opens a file that exists. The connection closes before the +/// driver opens the file. +pub fn create_database(path: &Path) -> Result<(), duckdb::Error> { + duckdb::Connection::open(path)?.execute_batch("CHECKPOINT") +} + +/// A database file of the case's own, which exists before the driver +/// opens it: the suite never relies on the driver creating one. +pub struct DuckDbEngine { + _directory: TempDir, + endpoint: FileEndpoint, +} + +fn quote(name: &str) -> String { + format!("\"{}\"", name.replace('"', "\"\"")) +} + +#[async_trait] +impl ConformanceEngine for DuckDbEngine { + const READ_ONLY_BESIDE_WRITER: bool = false; + + fn driver() -> Arc { + Arc::new(DuckDbDriver) + } + + async fn start() -> Result { + let directory = tempfile::tempdir().map_err(ConformanceFailure::setup)?; + let path = directory.path().join("conformance.duckdb"); + create_database(&path).map_err(ConformanceFailure::setup)?; + let endpoint = FileEndpoint::open_existing(path).map_err(ConformanceFailure::setup)?; + Ok(Self { + _directory: directory, + endpoint, + }) + } + + fn connect_options(&self, settings: WorkspaceSettings, liveness: LivenessPolicy) -> ConnectOptions { + ConnectOptions { + target: ConnectTarget::File(self.endpoint.clone()), + workspace_settings: settings, + liveness, + environment: ConnectEnvironment::new("conformance"), + } + } + + fn scratch_table(&self, name: &str) -> TableRef { + TableRef::unqualified(name) + } + + fn create_scratch_table_sql(&self, table: &TableRef) -> String { + format!( + "CREATE TABLE {} (id integer primary key, label text not null)", + quote(&table.name) + ) + } + + /// A cross join far too large to finish, which DuckDB checks for an + /// interrupt as it works through. + fn unbounded_statement_sql(&self, marker: &str) -> String { + format!("SELECT sum(a.range * b.range), '{marker}' FROM range(100000000) a, range(100000000) b") + } + + async fn blocking_statement_sql(&self, marker: &str, hold: Duration) -> Result { + Ok(format!("SELECT pg_sleep({}), '{marker}'", hold.as_secs_f64())) + } + + fn row_source_sql(&self, rows: u64) -> String { + format!("SELECT range + 1 AS n FROM range({rows})") + } +} + +impl PooledConformanceEngine for DuckDbEngine { + const WORKSPACE_POOL_MAX_SIZE: usize = tablepro_driver_duckdb::WORKSPACE_POOL_MAX_SIZE; +} + +impl EditorTransactionEngine for DuckDbEngine { + fn begin_sql(&self) -> String { + "BEGIN".to_owned() + } + + fn commit_sql(&self) -> String { + "COMMIT".to_owned() + } +} + +impl TypedConformanceEngine for DuckDbEngine { + /// Arrow carries a TIME WITH TIME ZONE without its offset, and a + /// BIGNUM in DuckDB's own encoding, so an editor run reads neither. + fn unsupported_type_cases(&self) -> Vec { + vec![UnsupportedTypeCase { + setup_sql: Vec::new(), + declaration: "v TIMETZ".to_owned(), + insert_value_sql: "TIMETZ '12:30:00+05:30'".to_owned(), + }] + } +} + +impl ConformanceDdlEngine for DuckDbEngine { + fn integer_type(&self) -> &'static str { + "INTEGER" + } + + fn text_type(&self) -> &'static str { + "VARCHAR" + } + + fn timestamp_type(&self) -> &'static str { + "TIMESTAMP" + } + + fn current_timestamp_expression(&self) -> &'static str { + "current_timestamp" + } +} + +impl LargeTableEngine for DuckDbEngine { + fn fill_scratch_table_sql(&self, table: &TableRef, rows: u64, label_width: usize) -> Vec { + vec![format!( + "INSERT INTO {} (id, label) SELECT range + 1, lpad(CAST(range + 1 AS VARCHAR), {label_width}, '0') \ + FROM range({rows})", + quote(&table.name) + )] + } +} diff --git a/linux/crates/drivers/duckdb/tests/integration/main.rs b/linux/crates/drivers/duckdb/tests/integration/main.rs new file mode 100644 index 0000000000..5f631b6661 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/main.rs @@ -0,0 +1,26 @@ +//! The DuckDB driver against real database files. No server is needed, +//! so these run in the fast CI job. +//! +//! Every module is test-only code: `cfg(test)` is what lets the shared +//! helpers fail a case with `unwrap`, as the case bodies do. + +#[cfg(test)] +mod catalog; +#[cfg(test)] +mod conformance_engine; +#[cfg(test)] +mod ddl; +#[cfg(test)] +mod decode; +#[cfg(test)] +mod duckdb_engine; +#[cfg(test)] +mod open_semantics; +#[cfg(test)] +mod round_trip; +#[cfg(test)] +mod sessions; +#[cfg(test)] +mod support; +#[cfg(test)] +mod value_corpus; diff --git a/linux/crates/drivers/duckdb/tests/integration/open_semantics.rs b/linux/crates/drivers/duckdb/tests/integration/open_semantics.rs new file mode 100644 index 0000000000..30256cd859 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/open_semantics.rs @@ -0,0 +1,340 @@ +//! What opening a path does: open what is there, create only when asked, +//! replace atomically, and keep one database per file in the process. + +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::fs::PermissionsExt; +use std::process::{Command, Stdio}; + +use tablepro_core::{ + BusyReason, DriverError, EditorSessionState, FileEndpoint, ReadOnlyRefusal, ReadStatement, RunEnd, ServerCode, + UserSql, Value, WorkspaceSettings, +}; + +use crate::support::{Collected, TestFile, apply, call, connect, query, run, session, writable}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_missing_file_is_not_found_and_not_created() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("missing.duckdb"); + + let opened = connect(FileEndpoint::open_existing(path.clone()).unwrap(), writable()).await; + + assert!( + matches!(&opened, Err(DriverError::FileNotFound { path: missing }) if *missing == path), + "{:?}", + opened.err() + ); + assert!(!path.exists(), "opening a missing path created a database"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_new_database_replaces_the_file_and_its_log() { + let file = TestFile::new(); + let old = file.open().await; + apply(old.as_ref(), &["CREATE TABLE old_table (a INTEGER)"]) + .await + .unwrap(); + old.close().await.unwrap(); + drop(old); + let wal = format!("{}.wal", file.path.display()); + std::fs::write(&wal, b"a log left from the replaced database").unwrap(); + + let created = connect(FileEndpoint::create_new(file.path.clone()).unwrap(), writable()) + .await + .unwrap(); + + let tables = query(created.as_ref(), "SELECT count(*) FROM duckdb_tables()").await; + assert_eq!(tables.rows[0][0], Value::Int(0), "the old tables survived the replace"); + let mode = std::fs::metadata(&file.path).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o600); + created.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_file_that_is_no_database_says_so() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("notes.duckdb"); + std::fs::write(&path, "plain text that is long enough to hold a database header").unwrap(); + + let opened = connect(FileEndpoint::open_existing(path.clone()).unwrap(), writable()).await; + + assert_eq!(opened.err(), Some(DriverError::NotADatabase { path })); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_read_only_connection_is_refused_by_duckdb_itself() { + let file = TestFile::new(); + let writer = file.open().await; + apply(writer.as_ref(), &["CREATE TABLE t (a INTEGER)"]).await.unwrap(); + writer.close().await.unwrap(); + drop(writer); + let reader = connect(file.endpoint(), WorkspaceSettings::workspace(true)) + .await + .unwrap(); + let editor = session(reader.as_ref()).await; + + let saved = apply(reader.as_ref(), &["INSERT INTO t VALUES (1)"]).await; + let (summary, _) = run(editor.as_ref(), "INSERT INTO t VALUES (1)").await; + + assert_eq!(saved.err(), Some(DriverError::ReadOnly(ReadOnlyRefusal::ClientGuard))); + assert!( + matches!( + &summary.end, + RunEnd::Failed(DriverError::ReadOnly(ReadOnlyRefusal::ServerRejected(_))) + ), + "{:?}", + summary.end + ); + editor.close().await; + reader.close().await.unwrap(); +} + +/// Two windows on one file share the database DuckDB keeps for it: a +/// second instance would keep a catalogue and a log of its own. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn two_workspaces_on_one_file_share_its_database() { + let file = TestFile::new(); + let first = file.open().await; + let second = file.open().await; + + apply( + first.as_ref(), + &["CREATE TABLE shared (a INTEGER)", "INSERT INTO shared VALUES (7)"], + ) + .await + .unwrap(); + let read = query(second.as_ref(), "SELECT a FROM shared").await; + let read_only = connect(file.endpoint(), WorkspaceSettings::workspace(true)).await; + + assert_eq!(read.rows, vec![vec![Value::Int(7)]]); + assert_eq!(read_only.err(), Some(DriverError::Busy(BusyReason::FileLocked))); + first.close().await.unwrap(); + second.close().await.unwrap(); +} + +/// An editor tab keeps its connection after the workspace closes, and +/// the file is let go once both are gone. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_editor_session_outlives_its_workspace() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + connection.close().await.unwrap(); + drop(connection); + + let (summary, collected) = run(editor.as_ref(), "SELECT 42 AS answer").await; + assert_eq!(summary.end, RunEnd::Completed); + assert_eq!(collected.rows(), vec![vec![Value::Int(42)]]); + editor.close().await; + assert_eq!(editor.state(), EditorSessionState::Lost); + drop(editor); + + let replaced = connect(FileEndpoint::create_new(file.path.clone()).unwrap(), writable()).await; + assert!(replaced.is_ok(), "the file was still held: {:?}", replaced.err()); +} + +/// The app makes no requests of its own, so DuckDB fetches and loads no +/// extension by itself. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn extensions_are_never_installed_or_loaded_on_their_own() { + let file = TestFile::new(); + let connection = file.open().await; + + let settings = query( + connection.as_ref(), + "SELECT current_setting('autoinstall_known_extensions'), current_setting('autoload_known_extensions')", + ) + .await; + + assert_eq!(settings.rows, vec![vec![Value::Bool(false), Value::Bool(false)]]); + connection.close().await.unwrap(); +} + +/// An export of the user's SQL runs in a read-only transaction, which +/// holds only while that SQL leaves the transaction alone. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_exported_query_cannot_write() { + let file = TestFile::new(); + let connection = file.open().await; + apply(connection.as_ref(), &["CREATE TABLE t (a INTEGER)"]) + .await + .unwrap(); + let export = |sql: &str| { + let read = ReadStatement::User(UserSql::new(sql)); + let connection = connection.clone(); + async move { connection.stream_read(&read, Collected::default().sink(), call()).await } + }; + + let committed = export("SELECT 1; COMMIT; INSERT INTO t VALUES (1)").await; + let inserted = export("INSERT INTO t VALUES (2)").await; + let read = export("SELECT 'COMMIT' AS word").await; + + assert_eq!( + committed.err(), + Some(DriverError::ReadOnly(ReadOnlyRefusal::ClientGuard)) + ); + assert!( + matches!( + &inserted, + Err(DriverError::ReadOnly(ReadOnlyRefusal::ServerRejected(_))) + ), + "{inserted:?}" + ); + assert_eq!(read.unwrap().rows, 1); + let rows = query(connection.as_ref(), "SELECT count(*) FROM t").await; + assert_eq!(rows.rows, vec![vec![Value::Int(0)]]); + connection.close().await.unwrap(); +} + +/// A hard link is the same file, so it joins the database already open. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_second_name_for_the_file_shares_its_database() { + let file = TestFile::new(); + let linked = file.path.with_file_name("linked.duckdb"); + std::fs::hard_link(&file.path, &linked).unwrap(); + let first = file.open().await; + let second = connect(FileEndpoint::open_existing(linked).unwrap(), writable()) + .await + .unwrap(); + + apply(first.as_ref(), &["CREATE TABLE shared (a INTEGER)"]) + .await + .unwrap(); + let read = query(second.as_ref(), "SELECT count(*) FROM shared").await; + + assert_eq!(read.rows, vec![vec![Value::Int(0)]]); + first.close().await.unwrap(); + second.close().await.unwrap(); +} + +/// An editor tab attaching a file another window has open would make a +/// second database on it, and so would a window opening a file a tab +/// has attached. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_file_open_in_one_place_is_not_opened_in_another() { + let file = TestFile::new(); + let other = TestFile::new(); + let spare = TestFile::new(); + let connection = file.open().await; + let opened = other.open().await; + let editor = session(connection.as_ref()).await; + + let (attaching_open, _) = run(editor.as_ref(), &format!("ATTACH '{}' AS o", other.path.display())).await; + let (attaching_spare, _) = run(editor.as_ref(), &format!("ATTACH '{}' AS s", spare.path.display())).await; + let opening_attached = connect(spare.endpoint(), writable()).await; + + assert_eq!( + attaching_open.end, + RunEnd::Failed(DriverError::Busy(BusyReason::FileLocked)) + ); + assert_eq!(attaching_spare.end, RunEnd::Completed); + assert_eq!(opening_attached.err(), Some(DriverError::Busy(BusyReason::FileLocked))); + editor.close().await; + opened.close().await.unwrap(); + connection.close().await.unwrap(); +} + +/// DuckDB reads an ATTACH path in every string form it has, and each +/// spelling of a file another window has open names that file. A +/// `U&'...'` string DuckDB refuses itself, as it refuses an expression. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn every_spelling_of_an_open_file_is_refused() { + let file = TestFile::new(); + let other = TestFile::new(); + let connection = file.open().await; + let opened = other.open().await; + let editor = session(connection.as_ref()).await; + let path = other.path.display().to_string(); + let (directory, name) = path.rsplit_once('/').unwrap(); + let spellings = [ + format!("ATTACH E'{}' AS o", path.replace('/', "\\x2f")), + format!("ATTACH E'{}' AS o", path.replace('/', "\\u002f")), + format!("ATTACH '{directory}/'\n'{name}' AS o"), + format!("ATTACH 'file://{path}' AS o"), + format!("ATTACH 'duckdb:{path}' AS o"), + format!("ATTACH IF NOT EXISTS $p${path}$p$ -- why\n AS o"), + ]; + + for sql in &spellings { + let (summary, _) = run(editor.as_ref(), sql).await; + assert_eq!( + summary.end, + RunEnd::Failed(DriverError::Busy(BusyReason::FileLocked)), + "{sql}" + ); + } + for sql in ["ATTACH 40 + 2 AS o", &format!("ATTACH U&'{path}' AS o")] { + let (malformed, _) = run(editor.as_ref(), sql).await; + assert!( + matches!( + &malformed.end, + RunEnd::Failed(DriverError::Server(diagnostics)) + if diagnostics.code == Some(ServerCode::DuckDb { error_type: "Parser".to_owned() }) + ), + "{sql}: {:?}", + malformed.end + ); + } + editor.close().await; + opened.close().await.unwrap(); + connection.close().await.unwrap(); +} + +const HOLDER: &str = "TABLEPRO_DUCKDB_TEST_HOLD"; +/// What the other process prints once it holds the file. The test +/// harness prints the test's name on the same line before it. +const HELD: &str = "tablepro-duckdb-test-held"; + +/// The other process of `a_file_another_process_has_open_is_not_replaced`: +/// it opens the file it is given and keeps it until its input closes. +#[test] +#[ignore = "the other process of a_file_another_process_has_open_is_not_replaced, which starts it"] +fn hold_a_file_open() { + let Some(path) = std::env::var_os(HOLDER) else { + return; + }; + let connection = duckdb::Connection::open(path).unwrap(); + writeln!(std::io::stdout(), "{HELD}").unwrap(); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + let _ = std::io::stdin().read_line(&mut line); + drop(connection); +} + +/// A database another process has open keeps its lock, and replacing it +/// would leave that process writing a log beside the new file. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_file_another_process_has_open_is_not_replaced() { + let file = TestFile::new(); + let mut holder = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "open_semantics::hold_a_file_open", + "--ignored", + "--nocapture", + "--test-threads=1", + ]) + .env(HOLDER, &file.path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut output = BufReader::new(holder.stdout.take().unwrap()); + let mut line = String::new(); + while !line.contains(HELD) { + line.clear(); + assert!(output.read_line(&mut line).unwrap() > 0, "the other process never opened the file"); + } + + let replaced = connect(FileEndpoint::create_new(file.path.clone()).unwrap(), writable()).await; + + drop(holder.stdin.take()); + let _ = std::io::copy(&mut output, &mut std::io::sink()); + holder.wait().unwrap(); + assert_eq!(replaced.err(), Some(DriverError::Busy(BusyReason::FileLocked))); + let kept = file.open().await; + let tables = query(kept.as_ref(), "SELECT count(*) FROM duckdb_tables()").await; + assert_eq!(tables.rows, vec![vec![Value::Int(0)]]); + kept.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/round_trip.rs b/linux/crates/drivers/duckdb/tests/integration/round_trip.rs new file mode 100644 index 0000000000..87b008dabd --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/round_trip.rs @@ -0,0 +1,11 @@ +//! The export round trip on DuckDB: the value corpus out as CSV and as +//! SQL INSERT and back into an empty copy, and a CSV near the import's +//! size limit. No server is needed, so it runs in the unit suite. + +use crate::duckdb_engine::DuckDbEngine; + +tablepro_test_support::round_trip_suite! { + engine: DuckDbEngine, + docker: false, + near_limit: true, +} diff --git a/linux/crates/drivers/duckdb/tests/integration/sessions.rs b/linux/crates/drivers/duckdb/tests/integration/sessions.rs new file mode 100644 index 0000000000..532cac4e9e --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/sessions.rs @@ -0,0 +1,282 @@ +//! Editor sessions on DuckDB: what a run reports, where its transaction +//! stands, and how a stop ends it. + +use std::future::Future; +use std::task::Poll; +use std::time::Duration; + +use tablepro_core::{ + EditorSessionState, ResultEvent, RowLimit, RunEnd, RunOptions, TransactionState, Truncation, Value, +}; +use tokio_util::sync::CancellationToken; + +use crate::support::{TestFile, run, run_with, session}; + +const UNBOUNDED: &str = "SELECT sum(a.range * b.range) FROM range(100000000) a, range(100000000) b"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_temporary_table_stays_with_its_tab() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let other = session(connection.as_ref()).await; + + run( + editor.as_ref(), + "CREATE TEMP TABLE scratch (a INTEGER); INSERT INTO scratch VALUES (7)", + ) + .await; + let (mine, collected) = run(editor.as_ref(), "SELECT a FROM scratch").await; + let (theirs, _) = run(other.as_ref(), "SELECT a FROM scratch").await; + + assert_eq!(mine.end, RunEnd::Completed); + assert_eq!(collected.rows(), vec![vec![Value::Int(7)]]); + assert!(matches!(theirs.end, RunEnd::Failed(_)), "{:?}", theirs.end); + editor.close().await; + other.close().await; + connection.close().await.unwrap(); +} + +/// DuckDB reports no autocommit flag, so where a transaction stands is +/// read from the transaction id across two statements. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_run_reports_where_its_transaction_stands() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let (idle, _) = run(editor.as_ref(), "CREATE TABLE t (id INTEGER PRIMARY KEY)").await; + let (open, _) = run(editor.as_ref(), "BEGIN; INSERT INTO t VALUES (1)").await; + let (failed, _) = run(editor.as_ref(), "INSERT INTO t VALUES (1)").await; + let (ended, _) = run(editor.as_ref(), "ROLLBACK").await; + + assert_eq!(idle.transaction_state, TransactionState::Idle); + assert_eq!(open.transaction_state, TransactionState::InTransaction); + assert!(matches!(failed.end, RunEnd::Failed(_)), "{:?}", failed.end); + assert_eq!(failed.transaction_state, TransactionState::Failed); + assert_eq!(ended.transaction_state, TransactionState::Idle); + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_statement_reports_rows_it_changed_or_returned() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let (_, created) = run(editor.as_ref(), "CREATE TABLE t (a INTEGER)").await; + let (_, inserted) = run(editor.as_ref(), "INSERT INTO t VALUES (1), (2), (3)").await; + let (_, returned) = run(editor.as_ref(), "DELETE FROM t WHERE a > 1 RETURNING a").await; + let (_, counted) = run(editor.as_ref(), "SELECT 5 AS \"Count\"").await; + + let commands = |events: Vec| -> Vec> { + events + .into_iter() + .filter_map(|event| match event { + ResultEvent::CommandCompleted { rows_affected } => Some(rows_affected), + _ => None, + }) + .collect() + }; + assert_eq!(commands(created.events()), [None]); + assert_eq!(commands(inserted.events()), [Some(3)]); + assert_eq!(returned.rows(), vec![vec![Value::Int(2)], vec![Value::Int(3)]]); + assert_eq!( + counted.rows(), + vec![vec![Value::Int(5)]], + "a SELECT of a Count column is rows" + ); + editor.close().await; + connection.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn the_row_limit_stops_reading_and_keeps_later_statements() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let limited = RunOptions::new(CancellationToken::new()).with_row_limit(RowLimit::new(10).unwrap()); + + let (summary, collected) = run_with(editor.as_ref(), "SELECT range FROM range(100000); SELECT 1", limited).await; + + assert_eq!(summary.end, RunEnd::Completed); + let events = collected.events(); + assert!( + events.contains(&ResultEvent::RowLimitReached { retained: 10 }), + "{events:?}" + ); + assert!(events.contains(&ResultEvent::ResultSetFinished { + retained: 10, + total_rows: None, + truncation: Truncation::ReadPastLimit, + })); + assert_eq!(collected.rows().len(), 11, "the limited set and the one after it"); + editor.close().await; + connection.close().await.unwrap(); +} + +/// A stop interrupts the running statement, and the session takes the +/// next run. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_cancelled_statement_is_interrupted() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let cancel = CancellationToken::new(); + let stopping = cancel.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(300)).await; + stopping.cancel(); + }); + + let (summary, _) = run_with(editor.as_ref(), UNBOUNDED, RunOptions::new(cancel)).await; + let (next, collected) = run(editor.as_ref(), "SELECT 1").await; + + assert_eq!(summary.end, RunEnd::Cancelled); + assert_eq!(editor.state(), EditorSessionState::Ready); + assert_eq!(next.end, RunEnd::Completed); + assert_eq!(collected.rows(), vec![vec![Value::Int(1)]]); + editor.close().await; + connection.close().await.unwrap(); +} + +/// The deadline reads as a timeout DuckDB acted on. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_statement_past_its_deadline_times_out() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let options = RunOptions::new(CancellationToken::new()).with_deadline(Some(Duration::from_millis(300))); + + let (summary, _) = run_with(editor.as_ref(), UNBOUNDED, options).await; + + assert_eq!(summary.end, RunEnd::TimedOut { server_cancelled: true }); + editor.close().await; + connection.close().await.unwrap(); +} + +/// DuckDB clears an interrupt as each statement starts, so a stop that +/// lands between statements must still keep the next one from running. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_stop_between_statements_runs_no_more() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + run(editor.as_ref(), "CREATE TABLE marks (a INTEGER)").await; + let cancel = CancellationToken::new(); + let stopping = cancel.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(300)).await; + stopping.cancel(); + }); + + let script = format!("SELECT count(*) FROM range(10); {UNBOUNDED}; INSERT INTO marks VALUES (1)"); + let (summary, _) = run_with(editor.as_ref(), &script, RunOptions::new(cancel)).await; + let (_, marks) = run(editor.as_ref(), "SELECT count(*) FROM marks").await; + + assert_eq!(summary.end, RunEnd::Cancelled); + assert_eq!( + marks.rows(), + vec![vec![Value::Int(0)]], + "a statement after the stop ran" + ); + editor.close().await; + connection.close().await.unwrap(); +} + +/// A run cancelled before it starts sends nothing. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_run_cancelled_before_it_starts_sends_nothing() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + run(editor.as_ref(), "CREATE TABLE marks (a INTEGER)").await; + let cancel = CancellationToken::new(); + cancel.cancel(); + + let (summary, _) = run_with(editor.as_ref(), "INSERT INTO marks VALUES (1)", RunOptions::new(cancel)).await; + let (_, marks) = run(editor.as_ref(), "SELECT count(*) FROM marks").await; + + assert_eq!(summary.end, RunEnd::Cancelled); + assert_eq!(marks.rows(), vec![vec![Value::Int(0)]]); + editor.close().await; + connection.close().await.unwrap(); +} + +/// A statement whose WITH clause leads to a row change reports the rows +/// it changed. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_change_after_a_with_clause_reports_its_count() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + run(editor.as_ref(), "CREATE TABLE t (a INTEGER)").await; + + let (_, inserted) = run( + editor.as_ref(), + "WITH src AS (SELECT range AS a FROM range(4)) INSERT INTO t SELECT a FROM src", + ) + .await; + + assert!( + inserted.events().contains(&ResultEvent::CommandCompleted { rows_affected: Some(4) }), + "{:?}", + inserted.events() + ); + editor.close().await; + connection.close().await.unwrap(); +} + +/// What a prepared statement or a table function returns is its rows, +/// whatever its one column is called. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_execute_or_a_call_returns_its_rows() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + run( + editor.as_ref(), + "PREPARE q AS SELECT 42 AS \"Count\"; \ + CREATE MACRO checked() AS TABLE SELECT true AS \"Success\"", + ) + .await; + + let (_, executed) = run(editor.as_ref(), "EXECUTE q").await; + let (_, called) = run(editor.as_ref(), "CALL checked()").await; + + assert_eq!(executed.rows(), vec![vec![Value::Int(42)]]); + assert_eq!(called.rows(), vec![vec![Value::Bool(true)]]); + editor.close().await; + connection.close().await.unwrap(); +} + +/// A caller that gives up a state read while it runs leaves the tab's +/// connection with that read, so the tab is lost rather than ready with +/// nothing to run on. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_state_read_given_up_loses_the_session() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + + let mut reading = Box::pin(editor.transaction_state(crate::support::call())); + let first = std::future::poll_fn(|context| Poll::Ready(reading.as_mut().poll(context))).await; + drop(reading); + let next = editor + .run( + tablepro_core::UserSql::new("SELECT 1"), + RunOptions::new(CancellationToken::new()), + crate::support::Collected::default().sink(), + ) + .await; + + assert!(first.is_pending(), "the state read finished before it was given up"); + assert_eq!(editor.state(), EditorSessionState::Lost); + assert!( + matches!(next, Err(tablepro_core::DriverError::ConnectionLost { .. })), + "{next:?}" + ); + editor.close().await; + connection.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/support.rs b/linux/crates/drivers/duckdb/tests/integration/support.rs new file mode 100644 index 0000000000..19b5f9a652 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/support.rs @@ -0,0 +1,152 @@ +//! What the DuckDB integration cases share: a database file that exists +//! before the driver opens it, connecting, and running plain SQL. + +use std::path::PathBuf; +use std::sync::{Arc, Mutex, PoisonError}; +use std::time::Duration; + +use async_trait::async_trait; +use tablepro_core::meta::TableRef; +use tablepro_core::statement::{GuardedStatement, Statement, StatementBuilder}; +use tablepro_core::{ + CallOptions, ConnectEnvironment, ConnectOptions, ConnectTarget, Connection, DatabaseDriver, DriverError, + EditorSession, EditorSessionOptions, FileEndpoint, ForeignKeyMode, QueryResult, ResultEvent, ResultSink, RowLimit, + RunOptions, RunSummary, SinkFlow, UserSql, Value, WorkspaceSettings, WriteBatch, WriteKind, WriteReport, +}; +use tablepro_driver_duckdb::DuckDbDriver; +use tablepro_test_support::conformance::test_liveness; +use tempfile::TempDir; +use tokio::time::Instant; +use tokio_util::sync::CancellationToken; + +use crate::duckdb_engine::create_database; + +/// A database file in a directory of its own, removed with it. +pub struct TestFile { + _directory: TempDir, + pub path: PathBuf, +} + +impl TestFile { + pub fn new() -> Self { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("test.duckdb"); + create_database(&path).unwrap(); + Self { + _directory: directory, + path, + } + } + + pub fn endpoint(&self) -> FileEndpoint { + FileEndpoint::open_existing(self.path.clone()).unwrap() + } + + pub async fn open(&self) -> Arc { + connect(self.endpoint(), writable()).await.unwrap() + } +} + +pub fn writable() -> WorkspaceSettings { + WorkspaceSettings::workspace(false) +} + +pub fn options(endpoint: FileEndpoint, settings: WorkspaceSettings) -> ConnectOptions { + ConnectOptions { + target: ConnectTarget::File(endpoint), + workspace_settings: settings, + liveness: test_liveness(), + environment: ConnectEnvironment::new("test"), + } +} + +pub async fn connect(endpoint: FileEndpoint, settings: WorkspaceSettings) -> Result, DriverError> { + DuckDbDriver + .connect(options(endpoint, settings), CancellationToken::new()) + .await +} + +/// A call bound loose enough for a loaded machine. +pub fn call() -> CallOptions { + CallOptions::new(Some(Instant::now() + Duration::from_secs(30)), CancellationToken::new()) +} + +pub fn statement(sql: &str) -> Statement { + let mut builder = StatementBuilder::new(); + builder.push_sql(sql); + builder.finish() +} + +pub fn batch(kind: WriteKind, statements: &[&str]) -> WriteBatch { + let steps = statements + .iter() + .map(|sql| GuardedStatement::unguarded(statement(sql))) + .collect(); + WriteBatch::new(kind, TableRef::unqualified("t"), steps, ForeignKeyMode::Enforced) +} + +/// Run DDL and data changes through the save path. +pub async fn apply(connection: &dyn Connection, statements: &[&str]) -> Result { + connection + .write_batch(&batch(WriteKind::SchemaChanges, statements), call()) + .await +} + +pub async fn query(connection: &dyn Connection, sql: &str) -> QueryResult { + connection + .query_bound(&statement(sql), RowLimit::EDITOR_DEFAULT, call()) + .await + .unwrap() +} + +/// The events one editor run sent. +#[derive(Debug, Clone, Default)] +pub struct Collected(Arc>>); + +impl Collected { + pub fn sink(&self) -> Box { + Box::new(CollectingSink(self.clone())) + } + + pub fn events(&self) -> Vec { + self.0.lock().unwrap_or_else(PoisonError::into_inner).clone() + } + + pub fn rows(&self) -> Vec> { + self.events() + .into_iter() + .filter_map(|event| match event { + ResultEvent::Rows(rows) => Some(rows), + _ => None, + }) + .flatten() + .collect() + } +} + +struct CollectingSink(Collected); + +#[async_trait] +impl ResultSink for CollectingSink { + async fn accept(&mut self, event: ResultEvent) -> SinkFlow { + self.0.0.lock().unwrap_or_else(PoisonError::into_inner).push(event); + SinkFlow::Continue + } +} + +pub async fn session(connection: &dyn Connection) -> Arc { + connection + .open_editor_session(EditorSessionOptions::default(), CancellationToken::new()) + .await + .unwrap() +} + +pub async fn run(session: &dyn EditorSession, sql: &str) -> (RunSummary, Collected) { + run_with(session, sql, RunOptions::new(CancellationToken::new())).await +} + +pub async fn run_with(session: &dyn EditorSession, sql: &str, options: RunOptions) -> (RunSummary, Collected) { + let collected = Collected::default(); + let summary = session.run(UserSql::new(sql), options, collected.sink()).await.unwrap(); + (summary, collected) +} diff --git a/linux/crates/drivers/duckdb/tests/integration/value_corpus.rs b/linux/crates/drivers/duckdb/tests/integration/value_corpus.rs new file mode 100644 index 0000000000..874ceb145f --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/value_corpus.rs @@ -0,0 +1,143 @@ +//! DuckDB's value corpus: its integers to 128 bits, decimals at their +//! scale, temporal values at their extremes, and the nested types a +//! table tab and an editor run read as DuckDB's own text. A UNION is not +//! here: its text names no member, so it has no SQL literal to export, +//! and `decode.rs` reads it. + +use tablepro_test_support::conformance::{CorpusColumn, ValueCorpusEngine}; + +use crate::duckdb_engine::DuckDbEngine; + +impl ValueCorpusEngine for DuckDbEngine { + fn value_corpus(&self) -> Vec { + let mut corpus = numbers(); + corpus.extend(texts()); + corpus.extend(temporal()); + corpus.extend(nested()); + corpus + } +} + +fn numbers() -> Vec { + vec![ + CorpusColumn::new("v BOOLEAN") + .shows("true", "true") + .shows("false", "false") + .null(), + CorpusColumn::new("v TINYINT").shows("-128", "-128").shows("127", "127"), + CorpusColumn::new("v UTINYINT").shows("255", "255"), + CorpusColumn::new("v INTEGER") + .shows("2147483647", "2147483647") + .shows("-2147483648", "-2147483648"), + CorpusColumn::new("v BIGINT") + .shows("9223372036854775807", "9223372036854775807") + .shows("-9223372036854775808", "-9223372036854775808"), + CorpusColumn::new("v UBIGINT").shows("18446744073709551615", "18446744073709551615"), + CorpusColumn::new("v HUGEINT") + .shows( + "170141183460469231731687303715884105727", + "170141183460469231731687303715884105727", + ) + .shows( + "-170141183460469231731687303715884105728", + "-170141183460469231731687303715884105728", + ), + CorpusColumn::new("v UHUGEINT").shows( + "340282366920938463463374607431768211455", + "340282366920938463463374607431768211455", + ), + CorpusColumn::new("v FLOAT").shows("1.5", "1.5").shows("0.1", "0.1"), + CorpusColumn::new("v DOUBLE") + .shows("0.1", "0.1") + .shows("-2.5e-5", "-0.000025") + .shows("'nan'::DOUBLE", "NaN") + .shows("'-inf'::DOUBLE", "-Infinity"), + CorpusColumn::new("v DECIMAL(18,3)") + .shows("1.5", "1.500") + .shows("-0.001", "-0.001") + .null(), + CorpusColumn::new("v DECIMAL(38,10)") + .shows( + "1234567890123456789012345678.1234567890", + "1234567890123456789012345678.1234567890", + ) + .shows("-12.5", "-12.5000000000"), + ] +} + +fn texts() -> Vec { + vec![ + CorpusColumn::new("v VARCHAR") + .shows("'naïve café'", "naïve café") + .shows("'日本語のテキスト'", "日本語のテキスト") + .shows("'🦀 emoji 👩‍💻'", "🦀 emoji 👩‍💻") + .shows("''", "") + .shows("'it''s'", "it's") + .shows("'line' || chr(10) || 'break'", "line\nbreak") + .null(), + CorpusColumn::new("v BLOB") + .shows("'\\xDE\\xAD\\xBE\\xEF'::BLOB", "0xdeadbeef") + .shows("''::BLOB", "0x"), + CorpusColumn::new("v UUID").shows( + "'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'", + "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + ), + CorpusColumn::new("v JSON").shows(r#"'{"b": 1, "a": [1, 2.50]}'"#, r#"{"b": 1, "a": [1, 2.50]}"#), + CorpusColumn::new("v BIT").shows("'10101'", "10101").shows("'0'", "0"), + CorpusColumn::new("v ENUM('sad', 'ok', 'happy')").shows("'happy'", "happy"), + ] +} + +fn temporal() -> Vec { + vec![ + CorpusColumn::new("v DATE") + .shows("'2024-02-29'", "2024-02-29") + .shows("'0044-03-15 (BC)'", "0044-03-15 BC") + .shows("'infinity'", "infinity") + .shows("'-infinity'", "-infinity"), + CorpusColumn::new("v TIME") + .shows("'12:30:00.123456'", "12:30:00.123456") + .shows("'00:00:00'", "00:00:00"), + CorpusColumn::new("v TIMESTAMP") + .shows("'2024-02-29 12:00:00.123'", "2024-02-29 12:00:00.123") + .shows("'1999-12-31 23:59:59'", "1999-12-31 23:59:59") + .shows("'infinity'", "infinity"), + CorpusColumn::new("v TIMESTAMP_NS").shows("'2024-01-01 00:00:00.123456789'", "2024-01-01 00:00:00.123456789"), + CorpusColumn::new("v TIMESTAMP_S").shows("'2024-01-01 00:00:01'", "2024-01-01 00:00:01"), + CorpusColumn::new("v TIMESTAMPTZ") + .shows("'2024-03-10 10:30:00+02'", "2024-03-10 08:30:00+00:00") + .shows("'2024-07-01 00:00:00.5+00'", "2024-07-01 00:00:00.5+00:00"), + CorpusColumn::new("v INTERVAL") + .shows("INTERVAL '1 year 2 months 3 days 04:05:06.789'", "P1Y2M3DT4H5M6.789S") + .shows("INTERVAL '-1 day'", "P-1D"), + // Arrow carries the time alone, so an editor run cannot read it. + CorpusColumn::new("v TIMETZ").shows_apart( + "'12:30:00+05:30'", + "12:30:00+05:30", + "", + ), + ] +} + +fn nested() -> Vec { + vec![ + CorpusColumn::new("v INTEGER[]") + .shows("[1, 2, NULL]", "[1, 2, NULL]") + .shows("[]", "[]"), + CorpusColumn::new("v VARCHAR[]").shows("['a', 'b c', '', 'it''s', 'x,y']", "[a, b c, '', 'it\\'s', 'x,y']"), + CorpusColumn::new("v INTEGER[3]").shows("[1, 2, 3]", "[1, 2, 3]"), + CorpusColumn::new("v STRUCT(a INTEGER, b VARCHAR)") + .shows("{'a': 1, 'b': 'x y'}", "{'a': 1, 'b': x y}") + .null(), + CorpusColumn::new("v MAP(VARCHAR, INTEGER)").shows("MAP {'k1': 1, 'k 2': NULL}", "{k1=1, k 2=NULL}"), + CorpusColumn::new("v STRUCT(d DECIMAL(10,2), t TIMESTAMP, l DOUBLE[])").shows( + "{'d': 1.5, 't': TIMESTAMP '2024-01-01 10:00:00', 'l': [1.0, 0.1]}", + "{'d': 1.50, 't': '2024-01-01 10:00:00', 'l': [1.0, 0.1]}", + ), + ] +} + +tablepro_test_support::value_corpus_suite! { + engine: DuckDbEngine, + docker: false, +} diff --git a/linux/crates/drivers/duckdb/tests/large_table.rs b/linux/crates/drivers/duckdb/tests/large_table.rs new file mode 100644 index 0000000000..7c519a3099 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/large_table.rs @@ -0,0 +1,12 @@ +//! The large-table case on DuckDB, a test binary of its own because it +//! bounds the process's resident peak. `cargo test` runs the tests of +//! one binary in one process, and the integration target's would count +//! toward the peak. Add no other test here. + +#[path = "integration/duckdb_engine.rs"] +mod duckdb_engine; + +tablepro_test_support::large_table_conformance_suite! { + engine: duckdb_engine::DuckDbEngine, + docker: false, +} diff --git a/linux/crates/drivers/mssql/src/ddl.rs b/linux/crates/drivers/mssql/src/ddl.rs index 329147470a..b4f4d5f7c3 100644 --- a/linux/crates/drivers/mssql/src/ddl.rs +++ b/linux/crates/drivers/mssql/src/ddl.rs @@ -28,6 +28,7 @@ static CAPABILITIES: DdlCapabilities = DdlCapabilities { drop_index: true, // An index key is a column; an expression needs a computed one. expression_index_keys: false, + descending_index_keys: true, partial_indexes: true, add_foreign_key: true, drop_foreign_key: true, diff --git a/linux/crates/drivers/mysql/src/ddl/mod.rs b/linux/crates/drivers/mysql/src/ddl/mod.rs index b5a415d7d1..99592e8a7e 100644 --- a/linux/crates/drivers/mysql/src/ddl/mod.rs +++ b/linux/crates/drivers/mysql/src/ddl/mod.rs @@ -31,6 +31,7 @@ const fn capabilities(expression_index_keys: bool) -> DdlCapabilities { create_index: true, drop_index: true, expression_index_keys, + descending_index_keys: true, partial_indexes: false, add_foreign_key: true, drop_foreign_key: true, diff --git a/linux/crates/drivers/postgres/src/ddl.rs b/linux/crates/drivers/postgres/src/ddl.rs index 0e178bddb5..5d802c550d 100644 --- a/linux/crates/drivers/postgres/src/ddl.rs +++ b/linux/crates/drivers/postgres/src/ddl.rs @@ -26,6 +26,7 @@ static CAPABILITIES: DdlCapabilities = DdlCapabilities { create_index: true, drop_index: true, expression_index_keys: true, + descending_index_keys: true, partial_indexes: true, add_foreign_key: true, drop_foreign_key: true, diff --git a/linux/crates/drivers/sqlite/src/capabilities.rs b/linux/crates/drivers/sqlite/src/capabilities.rs index 1a7baa82a6..38dc02416f 100644 --- a/linux/crates/drivers/sqlite/src/capabilities.rs +++ b/linux/crates/drivers/sqlite/src/capabilities.rs @@ -1,7 +1,20 @@ use tablepro_core::meta::ObjectKind; use tablepro_core::{ - CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, ReadOnlyEnforcement, ScriptExecution, - WriteAtomicity, + CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, FileFormat, FileSignature, + ReadOnlyEnforcement, ScriptExecution, WriteAtomicity, +}; + +/// A database file begins with this header, the shared MIME database +/// knows it by these types, and SQLite reads an empty file as an empty +/// database. +pub(crate) const FILE_FORMAT: FileFormat = FileFormat { + signature: FileSignature { + offset: 0, + bytes: b"SQLite format 3\0", + }, + suffixes: &["sqlite3", "sqlite", "db", "db3"], + mime_types: &["application/vnd.sqlite3", "application/x-sqlite3"], + empty_file_is_database: true, }; /// A file with no login and no TLS. A read-only connection is opened @@ -9,7 +22,7 @@ use tablepro_core::{ /// changes are one transaction each. A COMMIT never crosses a network, /// so there is nothing to resolve afterwards. pub(crate) static CAPABILITIES: DriverCapabilities = DriverCapabilities { - endpoint: EndpointKind::File, + endpoint: EndpointKind::File(FILE_FORMAT), auth_modes: &[], tls: None, read_only: ReadOnlyEnforcement::EngineEnforced, diff --git a/linux/crates/drivers/sqlite/src/ddl/mod.rs b/linux/crates/drivers/sqlite/src/ddl/mod.rs index f0c62d09ed..220c93d12d 100644 --- a/linux/crates/drivers/sqlite/src/ddl/mod.rs +++ b/linux/crates/drivers/sqlite/src/ddl/mod.rs @@ -49,6 +49,7 @@ static CAPABILITIES: DdlCapabilities = DdlCapabilities { create_index: true, drop_index: true, expression_index_keys: true, + descending_index_keys: true, partial_indexes: true, add_foreign_key: true, drop_foreign_key: true, diff --git a/linux/crates/test-support/src/conformance/cases/ddl.rs b/linux/crates/test-support/src/conformance/cases/ddl.rs index d1670d708a..e927ba0b43 100644 --- a/linux/crates/test-support/src/conformance/cases/ddl.rs +++ b/linux/crates/test-support/src/conformance/cases/ddl.rs @@ -175,7 +175,7 @@ pub async fn create_table_script_recreates_the_table( name: format!("{}_label", parent.name), keys: vec![IndexKey::Column { name: "label".to_owned(), - descending: true, + descending: ddl.capabilities().descending_index_keys, }], unique: false, primary: false, @@ -249,12 +249,10 @@ pub async fn structure_changes_keep_rows_and_indexes( primary: false, predicate: None, }; - step.apply( - &mut table, - "create index", - table_permission(ddl.capabilities().create_index), - |draft| draft.indexes.push(index), - ) + let permission = table_permission(ddl.table_capabilities(&table).create_index); + step.apply(&mut table, "create index", permission, |draft| { + draft.indexes.push(index) + }) .await?; let labels = ["first", "second", "third"]; let inserts = (1..) @@ -472,13 +470,9 @@ impl Step<'_> { ) -> Result<&'static str, ConformanceFailure> { let ddl = self.ddl; let note = DraftColumn::new("note", DraftType::Typed(text_type.to_owned())); - self.apply( - table, - "add column", - table_permission(ddl.capabilities().add_column), - |draft| draft.columns.push(note), - ) - .await?; + let permission = table_permission(ddl.table_capabilities(table).add_column); + self.apply(table, "add column", permission, |draft| draft.columns.push(note)) + .await?; let permission = column_permission(ddl, table, "note", |permissions| permissions.rename); self.apply(table, "rename column", permission, |draft| { @@ -518,13 +512,9 @@ impl Step<'_> { primary: false, predicate: None, }; - self.apply( - table, - "create index", - table_permission(self.ddl.capabilities().create_index), - |draft| draft.indexes.push(index), - ) - .await?; + let permission = table_permission(self.ddl.table_capabilities(table).create_index); + self.apply(table, "create index", permission, |draft| draft.indexes.push(index)) + .await?; Ok(()) } @@ -548,13 +538,11 @@ impl Step<'_> { on_update: None, }; let key_name = key.name.clone(); + let permission = table_permission(self.ddl.table_capabilities(&child_table).add_foreign_key); let added = self - .apply( - &mut child_table, - "add foreign key", - table_permission(self.ddl.capabilities().add_foreign_key), - |draft| draft.foreign_keys.push(key), - ) + .apply(&mut child_table, "add foreign key", permission, |draft| { + draft.foreign_keys.push(key) + }) .await?; if !added { return Ok(()); @@ -579,13 +567,9 @@ impl Step<'_> { ) -> Result { let renamed = TableRef::new(parent.schema.clone(), unique("ddl_renamed")); let new_name = renamed.name.clone(); + let permission = table_permission(self.ddl.table_capabilities(table).rename_table); let moved = self - .apply( - table, - "rename table", - table_permission(self.ddl.capabilities().rename_table), - |draft| draft.table.name = new_name, - ) + .apply(table, "rename table", permission, |draft| draft.table.name = new_name) .await?; if !moved { return Ok(parent); diff --git a/linux/crates/test-support/src/conformance/cases/read_only.rs b/linux/crates/test-support/src/conformance/cases/read_only.rs index 559800d609..ce6e7985e0 100644 --- a/linux/crates/test-support/src/conformance/cases/read_only.rs +++ b/linux/crates/test-support/src/conformance/cases/read_only.rs @@ -15,6 +15,13 @@ pub async fn f001_read_only_connection_refuses_writes( ) -> Result<(), ConformanceFailure> { let owner = connect(CASE, &engine, writable()).await?; let table = create_scratch_table(&engine, owner.as_ref(), "read_only").await?; + let owner = match E::READ_ONLY_BESIDE_WRITER { + true => Some(owner), + false => { + owner.close().await?; + None + } + }; let reader = connect(CASE, &engine, WorkspaceSettings::workspace(true)).await?; let insert = insert_sql(reader.dialect().as_ref(), &table, 1, "refused"); @@ -41,6 +48,13 @@ pub async fn f001_read_only_connection_refuses_writes( } session.close().await; } + let owner = match owner { + Some(owner) => owner, + None => { + reader.close().await?; + connect(CASE, &engine, writable()).await? + } + }; let rows = count(owner.as_ref(), &table).await?; expect(CASE, rows == 0, || { format!("{rows} rows reached a table only a read-only connection wrote to") diff --git a/linux/crates/test-support/src/conformance/cases/suite_flags.rs b/linux/crates/test-support/src/conformance/cases/suite_flags.rs index 4d52b7de17..89f8a96aa0 100644 --- a/linux/crates/test-support/src/conformance/cases/suite_flags.rs +++ b/linux/crates/test-support/src/conformance/cases/suite_flags.rs @@ -13,7 +13,7 @@ pub fn suite_flags_match_driver_capabilities( let capabilities = E::driver().capabilities(); let endpoint = match capabilities.endpoint { EndpointKind::Network { .. } => SuiteEndpoint::Network, - EndpointKind::File => SuiteEndpoint::File, + EndpointKind::File(_) => SuiteEndpoint::File, }; expect(CASE, flags.endpoint == endpoint, || { format!( diff --git a/linux/crates/test-support/src/conformance/conformance_engine.rs b/linux/crates/test-support/src/conformance/conformance_engine.rs index d9235d8154..1380871af3 100644 --- a/linux/crates/test-support/src/conformance/conformance_engine.rs +++ b/linux/crates/test-support/src/conformance/conformance_engine.rs @@ -13,6 +13,11 @@ use crate::conformance::ConformanceFailure; /// a bad state cannot fail the next one. #[async_trait] pub trait ConformanceEngine: Send + Sync + Sized + 'static { + /// Whether a read-only connection opens while a writable one holds + /// the database. DuckDB opens a file one way at a time, in one + /// process as in two. + const READ_ONLY_BESIDE_WRITER: bool = true; + fn driver() -> Arc; /// Start a server through tablepro-test-fixtures, or a file for an diff --git a/linux/crates/test-support/src/fake_driver.rs b/linux/crates/test-support/src/fake_driver.rs index 15e41477fd..ff77083849 100644 --- a/linux/crates/test-support/src/fake_driver.rs +++ b/linux/crates/test-support/src/fake_driver.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use tablepro_core::meta::ObjectKind; use tablepro_core::{ AuthMode, CommitResolutionSupport, ConnectOptions, Connection, DatabaseDriver, DriverCapabilities, DriverError, - EndpointKind, EstimateQuality, ReadOnlyEnforcement, ScriptExecution, ServerNameOverride, TimeoutPhase, - TlsCapabilities, TlsMode, WriteAtomicity, + EndpointKind, EstimateQuality, FileFormat, FileSignature, ReadOnlyEnforcement, ScriptExecution, ServerNameOverride, + TimeoutPhase, TlsCapabilities, TlsMode, WriteAtomicity, }; use tokio::time::Instant; use tokio_util::sync::CancellationToken; @@ -20,6 +20,7 @@ use crate::{ConnectionScript, FakeConnection, FakeReply, Replies, lock}; /// did with the result, including a late connect it should have closed. #[derive(Debug)] pub struct FakeDriver { + id: &'static str, capabilities: &'static DriverCapabilities, connects: Mutex>>, connections: Mutex>>, @@ -46,13 +47,40 @@ impl FakeDriver { database_required: false, }; + /// Files named as SQLite's are, with an empty file read as an empty + /// database, as SQLite reads one. pub const FILE_CAPABILITIES: DriverCapabilities = DriverCapabilities { - endpoint: EndpointKind::File, + endpoint: EndpointKind::File(FileFormat { + signature: FileSignature { + offset: 0, + bytes: b"Fake format 1\0", + }, + suffixes: &["sqlite3", "db"], + mime_types: &["application/vnd.sqlite3"], + empty_file_is_database: true, + }), auth_modes: &[], tls: None, ..Self::NETWORK_CAPABILITIES }; + /// A second file engine, whose files carry `SIGNED_FILE_SIGNATURE` + /// eight bytes in, as DuckDB's do, and never open empty. + pub const SIGNED_FILE_CAPABILITIES: DriverCapabilities = DriverCapabilities { + endpoint: EndpointKind::File(FileFormat { + signature: Self::SIGNED_FILE_SIGNATURE, + suffixes: &["duckdb"], + mime_types: &[], + empty_file_is_database: false, + }), + ..Self::FILE_CAPABILITIES + }; + + pub const SIGNED_FILE_SIGNATURE: FileSignature = FileSignature { + offset: 8, + bytes: b"DUCK", + }; + pub fn new( capabilities: &'static DriverCapabilities, connects: impl IntoIterator>, @@ -74,6 +102,7 @@ impl FakeDriver { fn with_replies(capabilities: &'static DriverCapabilities, connects: Replies>) -> Self { Self { + id: "fake", capabilities, connects: Mutex::new(connects), connections: Mutex::new(Vec::new()), @@ -81,6 +110,12 @@ impl FakeDriver { } } + /// The same driver under another id, so two fakes can share a + /// registry. + pub fn with_id(self, id: &'static str) -> Self { + Self { id, ..self } + } + pub fn connections(&self) -> Vec> { lock(&self.connections).clone() } @@ -93,7 +128,7 @@ impl FakeDriver { #[async_trait] impl DatabaseDriver for FakeDriver { fn id(&self) -> &'static str { - "fake" + self.id } fn display_name(&self) -> &'static str { diff --git a/linux/data/app.tablepro.TablePro.desktop.in.in b/linux/data/app.tablepro.TablePro.desktop.in.in index d0684f79cd..31663b6ff8 100644 --- a/linux/data/app.tablepro.TablePro.desktop.in.in +++ b/linux/data/app.tablepro.TablePro.desktop.in.in @@ -2,8 +2,8 @@ # Translators: TablePro is the product name and is not translated. Name=TablePro GenericName=Database Client -Comment=Browse and edit PostgreSQL, MySQL, SQLite, SQL Server and ClickHouse databases -Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;SQL Server;ClickHouse; +Comment=Browse and edit PostgreSQL, MySQL, SQLite, DuckDB, SQL Server and ClickHouse databases +Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;DuckDB;SQL Server;ClickHouse; Exec=tablepro %U Icon=@app-id@ Terminal=false diff --git a/linux/data/app.tablepro.TablePro.metainfo.xml.in.in b/linux/data/app.tablepro.TablePro.metainfo.xml.in.in index 6f7f2c8e08..9a311be8c3 100644 --- a/linux/data/app.tablepro.TablePro.metainfo.xml.in.in +++ b/linux/data/app.tablepro.TablePro.metainfo.xml.in.in @@ -8,8 +8,8 @@

Work with your databases

- TablePro connects to PostgreSQL, MySQL, SQLite, SQL Server and - ClickHouse. Browse tables, edit rows in place, run SQL with syntax + TablePro connects to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server + and ClickHouse. Browse tables, edit rows in place, run SQL with syntax highlighting, and reach servers behind an SSH bastion.

It can:

diff --git a/linux/data/licenses/THIRD-PARTY-NOTICE.md b/linux/data/licenses/THIRD-PARTY-NOTICE.md index 310140947b..1d0349f305 100644 --- a/linux/data/licenses/THIRD-PARTY-NOTICE.md +++ b/linux/data/licenses/THIRD-PARTY-NOTICE.md @@ -20,6 +20,17 @@ TablePro is licensed under the GNU Affero General Public License, version 3 or l Used by: +- arrow-arith 58.4.0 +- arrow-array 58.4.0 +- arrow-buffer 58.4.0 +- arrow-cast 58.4.0 +- arrow-data 58.4.0 +- arrow-ord 58.4.0 +- arrow-row 58.4.0 +- arrow-schema 58.4.0 +- arrow-select 58.4.0 +- arrow-string 58.4.0 +- arrow 58.4.0 - flagset 0.4.7 - fragile 2.1.0 @@ -3205,6 +3216,41 @@ THE SOFTWARE. Used by: +- cast 0.3.0 + +~~~~text +Copyright (c) 2014-2017 Jorge Aparicio + +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. + +~~~~ + +### MIT License + +Used by: + - flate2 1.1.10 ~~~~text @@ -3343,6 +3389,8 @@ Used by: - unicode-bidi 0.3.18 - unicode-normalization 0.1.25 - unicode-properties 0.1.4 +- unicode-segmentation 1.13.3 +- unicode-width 0.2.2 ~~~~text Copyright (c) 2015 The Rust Project Developers @@ -3378,6 +3426,7 @@ DEALINGS IN THE SOFTWARE. Used by: - fallible-iterator 0.2.0 +- fallible-iterator 0.3.0 ~~~~text Copyright (c) 2015 The rust-openssl-verify Developers @@ -3545,6 +3594,7 @@ DEALINGS IN THE SOFTWARE. Used by: +- hashbrown 0.15.5 - hashbrown 0.16.1 - hashbrown 0.17.1 @@ -3656,6 +3706,35 @@ DEALINGS IN THE SOFTWARE. Used by: +- fallible-streaming-iterator 0.1.9 + +~~~~text +Copyright (c) 2016 The fallible-streaming-iterator Developers + +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. + +~~~~ + +### MIT License + +Used by: + - indexmap 2.14.2 ~~~~text @@ -4351,6 +4430,41 @@ DEALINGS IN THE SOFTWARE. Used by: +- ahash 0.8.12 + +~~~~text +Copyright (c) 2018 Tom Kaitchuck + +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. + +~~~~ + +### MIT License + +Used by: + - want 0.3.1 ~~~~text @@ -5922,6 +6036,24 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI Used by: +- duckdb 1.10506.1 +- libduckdb-sys 1.10506.1 + +~~~~text +Copyright 2021-2026 Stichting DuckDB Foundation + +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. + +~~~~ + +### MIT License + +Used by: + - crypto-primes 0.7.2 ~~~~text @@ -6231,6 +6363,37 @@ SOFTWARE. Used by: +- comfy-table 7.1.4 + +~~~~text +MIT License + +Copyright (c) 2019 Arne Beer + +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. + +~~~~ + +### MIT License + +Used by: + - tokio-postgres-rustls 0.14.0 ~~~~text @@ -6262,6 +6425,38 @@ SOFTWARE. Used by: +- strum 0.27.2 +- strum_macros 0.27.2 + +~~~~text +MIT License + +Copyright (c) 2019 Peter Glotfelty + +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. + +~~~~ + +### MIT License + +Used by: + - tokio-macros 2.7.2 ~~~~text @@ -6356,6 +6551,37 @@ SOFTWARE. Used by: +- arrow-array 58.4.0 + +~~~~text +MIT License + +Copyright (c) 2020-2022 Oliver Margetts + +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. + +~~~~ + +### MIT License + +Used by: + - rustls-platform-verifier 0.7.0 ~~~~text @@ -6634,6 +6860,37 @@ SOFTWARE. Used by: +- crossterm 0.28.1 + +~~~~text +MIT License + +Copyright (c) 2019 Timon + +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. + +~~~~ + +### MIT License + +Used by: + - async-recursion 1.1.1 - flume 0.12.0 - rsa 0.10.0-rc.18 @@ -6680,7 +6937,14 @@ Used by: - fastrand 2.5.0 - futures-lite 2.6.1 - itoa 1.0.18 +- lexical-core 1.0.6 +- lexical-parse-float 1.0.6 +- lexical-parse-integer 1.0.6 +- lexical-util 1.0.7 +- lexical-write-float 1.0.6 +- lexical-write-integer 1.0.6 - linux-raw-sys 0.12.1 +- linux-raw-sys 0.4.15 - once_cell 1.21.4 - ordered-stream 0.2.0 - parking 2.2.1 @@ -6690,6 +6954,7 @@ Used by: - quote 1.0.47 - relm4-macros 0.11.0 - relm4 0.11.0 +- rustix 0.38.44 - rustix 1.1.4 - rustversion 1.0.23 - serde 1.0.229 @@ -7415,6 +7680,7 @@ SOFTWARE. Used by: +- hashlink 0.10.0 - hashlink 0.11.1 ~~~~text @@ -7874,6 +8140,7 @@ freely, subject to the following restrictions: Used by: +- foldhash 0.1.5 - foldhash 0.2.0 ~~~~text @@ -7897,3 +8164,2200 @@ the following restrictions: 3. This notice may not be removed or altered from any source distribution. ~~~~ + +## Code compiled into crates + +These crates compile C and C++ code from other projects into TablePro. Each project is listed under the license it is used under, with the file its text comes from. + +### DuckDB: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright 2018-2025 Stichting DuckDB Foundation + +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. +~~~~ + +### Brotli: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/brotli/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2009, 2010, 2013-2016 by the Brotli 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. +~~~~ + +### concurrentqueue: BSD 2-Clause "Simplified" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/concurrentqueue/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +This license file applies to everything in this repository except that which +is explicitly annotated as being written by other authors, i.e. the Boost +queue (included in the benchmarks for comparison), Intel's TBB library (ditto), +the CDSChecker tool (used for verification), the Relacy model checker (ditto), +and Jeff Preshing's semaphore implementation (used in the blocking queue) which +has a zlib license (embedded in lightweightsempahore.h). + +--- + +Simplified BSD License: + +Copyright (c) 2013-2016, Cameron Desrochers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of +conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this list of +conditions and the following disclaimer in the documentation and/or other materials +provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +I have also chosen to dual-license under the Boost Software License as an alternative to +the Simplified BSD license above: + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +~~~~ + +### fast_float: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/fast_float/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +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. +~~~~ + +### FastPFor: Apache License 2.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/fastpforlib/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +~~~~ + +### fmt: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/fmt/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2012 - present, Victor Zverovich + +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. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. +~~~~ + +### FSST: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/fsst/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +MIT License + +Copyright (c) 2018-2020, CWI, TU Munich, FSU Jena + +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. +~~~~ + +### cpp-httplib: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/httplib/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +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. +~~~~ + +### HyperLogLog: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/hyperloglog/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2012 Art.sy, Inc. + +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. +~~~~ + +### jaro_winkler: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/jaro_winkler/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright © 2022 Max Bachmann + +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. +~~~~ + +### libpg_query: BSD 3-Clause "New" or "Revised" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/libpg_query/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2015, Lukas Fittl +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +* Neither the name of pg_query nor the names of its contributors may be used +to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +~~~~ + +### LZ4: BSD 2-Clause "Simplified" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/lz4/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +LZ4 Library +Copyright (c) 2011-2020, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +~~~~ + +### Mbed TLS: Apache License 2.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/mbedtls/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) +OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license. +This means that users may choose which of these licenses they take the code +under. + +The full text of each of these licenses is given below. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +=============================================================================== + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. +~~~~ + +### miniz: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/miniz/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + +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. +~~~~ + +### Apache Parquet: Apache License 2.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/parquet/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +-------------------------------------------------------------------------------- + +This product includes code from Apache Avro. + +Copyright: 2014 The Apache Software Foundation. +Home page: https://avro.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Daniel Lemire's JavaFastPFOR project. The +"Lemire" bit packing source code produced by parquet-generator is derived from +the JavaFastPFOR project. + +Copyright: 2013 Daniel Lemire +Home page: http://lemire.me/en/ +Project page: https://github.com/lemire/JavaFastPFOR +License: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This product includes code from Apache Spark. + +* dev/merge_parquet_pr.py is based on Spark's dev/merge_spark_pr.py + +Copyright: 2014 The Apache Software Foundation. +Home page: https://spark.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This product includes code from Twitter's ElephantBird project. + +* parquet-hadoop's UnmaterializableRecordCounter.java includes code from + ElephantBird's LzoRecordReader.java + +Copyright: 2012-2014 Twitter +Home page: https://github.com/twitter/elephant-bird +License: http://www.apache.org/licenses/LICENSE-2.0 +~~~~ + +### PCG: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/pcg/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2014-2017 Melissa O'Neill and PCG Project 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. +~~~~ + +### pdqsort: zlib License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/pdqsort/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright (c) 2021 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In no event will the +authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as + being the original software. + +3. This notice may not be removed or altered from any source distribution. +~~~~ + +### RE2: BSD 3-Clause "New" or "Revised" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/re2/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +// Copyright (c) 2009 The RE2 Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +~~~~ + +### ska_sort: Boost Software License 1.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/ska_sort/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text + Copyright Malte Skarupke 2016. + Distributed under the Boost Software License, Version 1.0. + (See http://www.boost.org/LICENSE_1_0.txt) +~~~~ + +### skiplist: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/skiplist/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +MIT License + +Copyright (c) 2017-2023 Paul Ross + +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. +~~~~ + +### Snappy: BSD 3-Clause "New" or "Revised" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/snappy/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in testdata/ is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). +~~~~ + +### t-digest: Apache License 2.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/tdigest/LICENSE and NOTICES` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +The Java version of the t-digest was originally authored by Ted Dunning + +A number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) to the Java version. + +The C++ version herein is a derivative of the Java version. It was written by Derrick R. Burns (https:://github.com/derrickburns). +The main modifications are 1) higher performance multi- t-digest merging and 2) faster quantile() and cdf() computation. +~~~~ + +### Apache Thrift: Apache License 2.0 + +Compiled into libduckdb-sys 1.10506.1, from `third_party/thrift/thrift/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------- +SOFTWARE DISTRIBUTED WITH THRIFT: + +The Apache Thrift software includes a number of subcomponents with +separate copyright notices and license terms. Your use of the source +code for the these subcomponents is subject to the terms and +conditions of the following licenses. + +-------------------------------------------------- +Portions of the following files are licensed under the MIT License: + + lib/erl/src/Makefile.am + +Please see doc/otp-base-license.txt for the full terms of this license. + +-------------------------------------------------- +For the aclocal/ax_boost_base.m4 and contrib/fb303/aclocal/ax_boost_base.m4 components: + +# Copyright (c) 2007 Thomas Porschberg +# +# Copying and distribution of this file, with or without +# modification, are permitted in any medium without royalty provided +# the copyright notice and this notice are preserved. + +-------------------------------------------------- +For the lib/nodejs/lib/thrift/json_parse.js: + +/* + json_parse.js + 2015-05-02 + Public Domain. + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +*/ +(By Douglas Crockford ) +-------------------------------------------------- +~~~~ + +### utf8proc: Unicode License Agreement - Data Files and Software (2015) + +Compiled into libduckdb-sys 1.10506.1, from `third_party/utf8proc/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +## utf8proc license ## + +**utf8proc** is a software package originally developed +by Jan Behrens and the rest of the Public Software Group, who +deserve nearly all of the credit for this library, that is now maintained by the Julia-language developers. Like the original utf8proc, +whose copyright and license statements are reproduced below, all new +work on the utf8proc library is licensed under the [MIT "expat" +license](http://opensource.org/licenses/MIT): + +*Copyright © 2014-2019 by Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas Fonseca, and other contributors listed in the git history.* + +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. + +## Original utf8proc license ## + +*Copyright (c) 2009, 2013 Public Software Group e. V., Berlin, Germany* + +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. + +## Unicode data license ## + +This software contains data (`utf8proc_data.c`) derived from processing +the Unicode data files. The following license applies to that data: + +**COPYRIGHT AND PERMISSION NOTICE** + +*Copyright (c) 1991-2007 Unicode, Inc. All rights reserved. Distributed +under the Terms of Use in http://www.unicode.org/copyright.html.* + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Unicode data files and any associated documentation (the "Data +Files") or Unicode software and any associated documentation (the +"Software") to deal in the Data Files or Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, and/or sell copies of the Data Files or Software, and +to permit persons to whom the Data Files or Software are furnished to do +so, provided that (a) the above copyright notice(s) and this permission +notice appear with all copies of the Data Files or Software, (b) both the +above copyright notice(s) and this permission notice appear in associated +documentation, and (c) there is clear notice in each modified Data File or +in the Software as well as in the documentation associated with the Data +File(s) or Software that the data or software has been modified. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS +INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL 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 THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be +registered in some jurisdictions. All other trademarks and registered +trademarks mentioned herein are the property of their respective owners. +~~~~ + +### vergesort: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/vergesort/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +The MIT License (MIT) + +Copyright (c) 2015 Morwenn + +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. +~~~~ + +### yyjson: MIT License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/yyjson/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +MIT License + +Copyright (c) 2020 YaoYuan + +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. +~~~~ + +### Zstandard: BSD 3-Clause "New" or "Revised" License + +Compiled into libduckdb-sys 1.10506.1, from `third_party/zstd/LICENSE` in https://github.com/duckdb/duckdb/tree/069cc9f9b5be802405797faecc284961b07c70ef. + +~~~~text +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +~~~~ diff --git a/linux/data/licenses/third-party.json b/linux/data/licenses/third-party.json index e6b97e1f3c..e83b9fcaf0 100644 --- a/linux/data/licenses/third-party.json +++ b/linux/data/licenses/third-party.json @@ -5,6 +5,17 @@ "name": "Apache License 2.0", "text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", "used_by": [ + { "crate": { "name": "arrow-arith", "version": "58.4.0" } }, + { "crate": { "name": "arrow-array", "version": "58.4.0" } }, + { "crate": { "name": "arrow-buffer", "version": "58.4.0" } }, + { "crate": { "name": "arrow-cast", "version": "58.4.0" } }, + { "crate": { "name": "arrow-data", "version": "58.4.0" } }, + { "crate": { "name": "arrow-ord", "version": "58.4.0" } }, + { "crate": { "name": "arrow-row", "version": "58.4.0" } }, + { "crate": { "name": "arrow-schema", "version": "58.4.0" } }, + { "crate": { "name": "arrow-select", "version": "58.4.0" } }, + { "crate": { "name": "arrow-string", "version": "58.4.0" } }, + { "crate": { "name": "arrow", "version": "58.4.0" } }, { "crate": { "name": "flagset", "version": "0.4.7" } }, { "crate": { "name": "fragile", "version": "2.1.0" } } ] @@ -273,6 +284,14 @@ { "crate": { "name": "libsqlite3-sys", "version": "0.37.0" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2014-2017 Jorge Aparicio\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n", + "used_by": [ + { "crate": { "name": "cast", "version": "0.3.0" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -313,7 +332,9 @@ { "crate": { "name": "heck", "version": "0.5.0" } }, { "crate": { "name": "unicode-bidi", "version": "0.3.18" } }, { "crate": { "name": "unicode-normalization", "version": "0.1.25" } }, - { "crate": { "name": "unicode-properties", "version": "0.1.4" } } + { "crate": { "name": "unicode-properties", "version": "0.1.4" } }, + { "crate": { "name": "unicode-segmentation", "version": "1.13.3" } }, + { "crate": { "name": "unicode-width", "version": "0.2.2" } } ] }, { @@ -321,7 +342,8 @@ "name": "MIT License", "text": "Copyright (c) 2015 The rust-openssl-verify Developers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "used_by": [ - { "crate": { "name": "fallible-iterator", "version": "0.2.0" } } + { "crate": { "name": "fallible-iterator", "version": "0.2.0" } }, + { "crate": { "name": "fallible-iterator", "version": "0.3.0" } } ] }, { @@ -369,6 +391,7 @@ "name": "MIT License", "text": "Copyright (c) 2016 Amanieu d'Antras\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n", "used_by": [ + { "crate": { "name": "hashbrown", "version": "0.15.5" } }, { "crate": { "name": "hashbrown", "version": "0.16.1" } }, { "crate": { "name": "hashbrown", "version": "0.17.1" } } ] @@ -394,6 +417,14 @@ { "crate": { "name": "thread_local", "version": "1.1.10" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2016 The fallible-streaming-iterator Developers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "used_by": [ + { "crate": { "name": "fallible-streaming-iterator", "version": "0.1.9" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -558,6 +589,14 @@ { "crate": { "name": "smallvec", "version": "1.16.1" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2018 Tom Kaitchuck\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n", + "used_by": [ + { "crate": { "name": "ahash", "version": "0.8.12" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -951,6 +990,15 @@ { "crate": { "name": "multiversion", "version": "0.9.0" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright 2021-2026 Stichting DuckDB Foundation\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n", + "used_by": [ + { "crate": { "name": "duckdb", "version": "1.10506.1" } }, + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -1034,6 +1082,14 @@ { "crate": { "name": "crc-catalog", "version": "2.5.0" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2019 Arne Beer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "used_by": [ + { "crate": { "name": "comfy-table", "version": "7.1.4" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -1042,6 +1098,15 @@ { "crate": { "name": "tokio-postgres-rustls", "version": "0.14.0" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2019 Peter Glotfelty\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "used_by": [ + { "crate": { "name": "strum", "version": "0.27.2" } }, + { "crate": { "name": "strum_macros", "version": "0.27.2" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -1066,6 +1131,14 @@ { "crate": { "name": "ashpd", "version": "0.13.13" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2020-2022 Oliver Margetts\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "used_by": [ + { "crate": { "name": "arrow-array", "version": "58.4.0" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -1142,6 +1215,14 @@ { "crate": { "name": "whoami", "version": "2.1.3" } } ] }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\r\n\r\nCopyright (c) 2019 Timon\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy\r\nof this software and associated documentation files (the \"Software\"), to deal\r\nin the Software without restriction, including without limitation the rights\r\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\ncopies of the Software, and to permit persons to whom the Software is\r\nfurnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all\r\ncopies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\nSOFTWARE.\r\n", + "used_by": [ + { "crate": { "name": "crossterm", "version": "0.28.1" } } + ] + }, { "id": "MIT", "name": "MIT License", @@ -1169,7 +1250,14 @@ { "crate": { "name": "fastrand", "version": "2.5.0" } }, { "crate": { "name": "futures-lite", "version": "2.6.1" } }, { "crate": { "name": "itoa", "version": "1.0.18" } }, + { "crate": { "name": "lexical-core", "version": "1.0.6" } }, + { "crate": { "name": "lexical-parse-float", "version": "1.0.6" } }, + { "crate": { "name": "lexical-parse-integer", "version": "1.0.6" } }, + { "crate": { "name": "lexical-util", "version": "1.0.7" } }, + { "crate": { "name": "lexical-write-float", "version": "1.0.6" } }, + { "crate": { "name": "lexical-write-integer", "version": "1.0.6" } }, { "crate": { "name": "linux-raw-sys", "version": "0.12.1" } }, + { "crate": { "name": "linux-raw-sys", "version": "0.4.15" } }, { "crate": { "name": "once_cell", "version": "1.21.4" } }, { "crate": { "name": "ordered-stream", "version": "0.2.0" } }, { "crate": { "name": "parking", "version": "2.2.1" } }, @@ -1179,6 +1267,7 @@ { "crate": { "name": "quote", "version": "1.0.47" } }, { "crate": { "name": "relm4-macros", "version": "0.11.0" } }, { "crate": { "name": "relm4", "version": "0.11.0" } }, + { "crate": { "name": "rustix", "version": "0.38.44" } }, { "crate": { "name": "rustix", "version": "1.1.4" } }, { "crate": { "name": "rustversion", "version": "1.0.23" } }, { "crate": { "name": "serde", "version": "1.0.229" } }, @@ -1412,6 +1501,7 @@ "name": "MIT License", "text": "This work is derived in part from the `linked-hash-map` crate, Copyright (c)\n2015 The Rust Project Developers\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.", "used_by": [ + { "crate": { "name": "hashlink", "version": "0.10.0" } }, { "crate": { "name": "hashlink", "version": "0.11.1" } } ] }, @@ -1474,14 +1564,270 @@ "name": "zlib License", "text": "Copyright (c) 2024 Orson Peters\n\nThis software is provided 'as-is', without any express or implied warranty. In\nno event will the authors be held liable for any damages arising from the use of\nthis software.\n\nPermission is granted to anyone to use this software for any purpose, including\ncommercial applications, and to alter it and redistribute it freely, subject to\nthe following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not claim\n that you wrote the original software. If you use this software in a product,\n an acknowledgment in the product documentation would be appreciated but is\n not required.\n\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n\n3. This notice may not be removed or altered from any source distribution.", "used_by": [ + { "crate": { "name": "foldhash", "version": "0.1.5" } }, { "crate": { "name": "foldhash", "version": "0.2.0" } } ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright 2018-2025 Stichting DuckDB Foundation\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n", + "component": "DuckDB", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n", + "component": "Brotli", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-2-Clause", + "name": "BSD 2-Clause \"Simplified\" License", + "text": "This license file applies to everything in this repository except that which\nis explicitly annotated as being written by other authors, i.e. the Boost\nqueue (included in the benchmarks for comparison), Intel's TBB library (ditto),\nthe CDSChecker tool (used for verification), the Relacy model checker (ditto),\nand Jeff Preshing's semaphore implementation (used in the blocking queue) which\nhas a zlib license (embedded in lightweightsempahore.h).\n\n---\n\nSimplified BSD License:\n\nCopyright (c) 2013-2016, Cameron Desrochers.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n- Redistributions of source code must retain the above copyright notice, this list of\nconditions and the following disclaimer.\n- Redistributions in binary form must reproduce the above copyright notice, this list of\nconditions and the following disclaimer in the documentation and/or other materials\nprovided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL\nTHE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR\nTORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---\n\nI have also chosen to dual-license under the Boost Software License as an alternative to\nthe Simplified BSD license above:\n\nBoost Software License - Version 1.0 - August 17th, 2003\n\nPermission is hereby granted, free of charge, to any person or organization\nobtaining a copy of the software and accompanying documentation covered by\nthis license (the \"Software\") to use, reproduce, display, distribute,\nexecute, and transmit the Software, and to prepare derivative works of the\nSoftware, and to permit third-parties to whom the Software is furnished to\ndo so, all subject to the following:\n\nThe copyright notices in the Software and this entire statement, including\nthe above license grant, this restriction and the following disclaimer,\nmust be included in all copies of the Software, in whole or in part, and\nall derivative works of the Software, unless such copies or derivative\nworks are solely in the form of machine-executable object code generated by\na source language processor.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT\nSHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE\nFOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n", + "component": "concurrentqueue", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Permission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n", + "component": "fast_float", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "text": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", + "component": "FastPFor", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2012 - present, Victor Zverovich\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n--- Optional exception to the license ---\n\nAs an exception, if, as a result of your compiling your source code, portions\nof this Software are embedded into a machine-executable object form of such\nsource code, you may redistribute such embedded portions in such object form\nwithout including the above copyright and permission notices.\n", + "component": "fmt", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2018-2020, CWI, TU Munich, FSU Jena\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "FSST", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "The MIT License (MIT)\n\nCopyright (c) 2017 yhirose\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "cpp-httplib", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2012 Art.sy, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "HyperLogLog", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright © 2022 Max Bachmann\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", + "component": "jaro_winkler", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-3-Clause", + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "text": "Copyright (c) 2015, Lukas Fittl \nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n* Neither the name of pg_query nor the names of its contributors may be used\nto endorse or promote products derived from this software without specific\nprior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n", + "component": "libpg_query", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-2-Clause", + "name": "BSD 2-Clause \"Simplified\" License", + "text": "LZ4 Library\nCopyright (c) 2011-2020, Yann Collet\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", + "component": "LZ4", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "text": "Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)\nOR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license.\nThis means that users may choose which of these licenses they take the code\nunder.\n\nThe full text of each of these licenses is given below.\n\n\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n\n===============================================================================\n\n\n GNU GENERAL PUBLIC LICENSE\n Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n Preamble\n\n The licenses for most software are designed to take away your\nfreedom to share and change it. By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users. This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it. (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.) You can apply it to\nyour programs, too.\n\n When we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have. You must make sure that they, too, receive or can get the\nsource code. And you must show them these terms so they know their\nrights.\n\n We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware. If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n Finally, any free program is threatened constantly by software\npatents. We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary. To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n The precise terms and conditions for copying, distribution and\nmodification follow.\n\n GNU GENERAL PUBLIC LICENSE\n TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n 0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License. The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage. (Hereinafter, translation is included without limitation in\nthe term \"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope. The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n 1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n 2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n a) You must cause the modified files to carry prominent notices\n stating that you changed the files and the date of any change.\n\n b) You must cause any work that you distribute or publish, that in\n whole or in part contains or is derived from the Program or any\n part thereof, to be licensed as a whole at no charge to all third\n parties under the terms of this License.\n\n c) If the modified program normally reads commands interactively\n when run, you must cause it, when started running for such\n interactive use in the most ordinary way, to print or display an\n announcement including an appropriate copyright notice and a\n notice that there is no warranty (or else, saying that you provide\n a warranty) and that users may redistribute the program under\n these conditions, and telling the user how to view a copy of this\n License. (Exception: if the Program itself is interactive but\n does not normally print such an announcement, your work based on\n the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works. But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n 3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n a) Accompany it with the complete corresponding machine-readable\n source code, which must be distributed under the terms of Sections\n 1 and 2 above on a medium customarily used for software interchange; or,\n\n b) Accompany it with a written offer, valid for at least three\n years, to give any third party, for a charge no more than your\n cost of physically performing source distribution, a complete\n machine-readable copy of the corresponding source code, to be\n distributed under the terms of Sections 1 and 2 above on a medium\n customarily used for software interchange; or,\n\n c) Accompany it with the information you received as to the offer\n to distribute corresponding source code. (This alternative is\n allowed only for noncommercial distribution and only if you\n received the program in object code or executable form with such\n an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it. For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable. However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n 4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License. Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n 5. You are not required to accept this License, since you have not\nsigned it. However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works. These actions are\nprohibited by law if you do not accept this License. Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n 6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions. You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n 7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all. For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices. Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n 8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded. In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n 9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time. Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation. If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n 10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission. For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this. Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n NO WARRANTY\n\n 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n END OF TERMS AND CONDITIONS\n\n How to Apply These Terms to Your New Programs\n\n If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n To do so, attach the following notices to the program. It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n \n Copyright (C) \n\n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNU General Public License for more details.\n\n You should have received a copy of the GNU General Public License along\n with this program; if not, write to the Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n Gnomovision version 69, Copyright (C) year name of author\n Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n This is free software, and you are welcome to redistribute it\n under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License. Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary. Here is a sample; alter the names:\n\n Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n , 1 April 1989\n Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs. If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary. If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n", + "component": "Mbed TLS", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright 2013-2014 RAD Game Tools and Valve Software\nCopyright 2010-2014 Rich Geldreich and Tenacious Software LLC\n\nAll Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n", + "component": "miniz", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro.\n\nCopyright: 2014 The Apache Software Foundation.\nHome page: https://avro.apache.org/\nLicense: http://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis project includes code from Daniel Lemire's JavaFastPFOR project. The\n\"Lemire\" bit packing source code produced by parquet-generator is derived from\nthe JavaFastPFOR project.\n\nCopyright: 2013 Daniel Lemire\nHome page: http://lemire.me/en/\nProject page: https://github.com/lemire/JavaFastPFOR\nLicense: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Spark.\n\n* dev/merge_parquet_pr.py is based on Spark's dev/merge_spark_pr.py\n\nCopyright: 2014 The Apache Software Foundation.\nHome page: https://spark.apache.org/\nLicense: http://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Twitter's ElephantBird project.\n\n* parquet-hadoop's UnmaterializableRecordCounter.java includes code from\n ElephantBird's LzoRecordReader.java\n\nCopyright: 2012-2014 Twitter\nHome page: https://github.com/twitter/elephant-bird\nLicense: http://www.apache.org/licenses/LICENSE-2.0\n", + "component": "Apache Parquet", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "Copyright (c) 2014-2017 Melissa O'Neill and PCG Project contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "PCG", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Zlib", + "name": "zlib License", + "text": "Copyright (c) 2021 Orson Peters \n\nThis software is provided 'as-is', without any express or implied warranty. In no event will the\nauthors be held liable for any damages arising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose, including commercial\napplications, and to alter it and redistribute it freely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not claim that you wrote the\n original software. If you use this software in a product, an acknowledgment in the product\n documentation would be appreciated but is not required.\n\n2. Altered source versions must be plainly marked as such, and must not be misrepresented as\n being the original software.\n\n3. This notice may not be removed or altered from any source distribution.\n", + "component": "pdqsort", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-3-Clause", + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "text": "// Copyright (c) 2009 The RE2 Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n// * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n// * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n// * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", + "component": "RE2", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSL-1.0", + "name": "Boost Software License 1.0", + "text": " Copyright Malte Skarupke 2016.\n Distributed under the Boost Software License, Version 1.0.\n (See http://www.boost.org/LICENSE_1_0.txt)\n", + "component": "ska_sort", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2017-2023 Paul Ross\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "skiplist", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-3-Clause", + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "text": "Copyright 2011, Google Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n===\n\nSome of the benchmark data in testdata/ is licensed differently:\n\n - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and\n is licensed under the Creative Commons Attribution 3.0 license\n (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/\n for more information.\n\n - kppkn.gtb is taken from the Gaviota chess tablebase set, and\n is licensed under the MIT License. See\n https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1\n for more information.\n\n - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper\n “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA\n Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro,\n which is licensed under the CC-BY license. See\n http://www.ploscompbiol.org/static/license for more ifnormation.\n\n - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project\n Gutenberg. The first three have expired copyrights and are in the public\n domain; the latter does not have expired copyright, but is still in the\n public domain according to the license information\n (http://www.gutenberg.org/ebooks/53).\n", + "component": "Snappy", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "text": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\nThe Java version of the t-digest was originally authored by Ted Dunning\n\nA number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) to the Java version.\n\nThe C++ version herein is a derivative of the Java version. It was written by Derrick R. Burns (https:://github.com/derrickburns).\nThe main modifications are 1) higher performance multi- t-digest merging and 2) faster quantile() and cdf() computation.\n", + "component": "t-digest", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n--------------------------------------------------\nSOFTWARE DISTRIBUTED WITH THRIFT:\n\nThe Apache Thrift software includes a number of subcomponents with\nseparate copyright notices and license terms. Your use of the source\ncode for the these subcomponents is subject to the terms and\nconditions of the following licenses.\n\n--------------------------------------------------\nPortions of the following files are licensed under the MIT License:\n\n lib/erl/src/Makefile.am\n\nPlease see doc/otp-base-license.txt for the full terms of this license.\n\n--------------------------------------------------\nFor the aclocal/ax_boost_base.m4 and contrib/fb303/aclocal/ax_boost_base.m4 components:\n\n# Copyright (c) 2007 Thomas Porschberg \n#\n# Copying and distribution of this file, with or without\n# modification, are permitted in any medium without royalty provided\n# the copyright notice and this notice are preserved.\n\n--------------------------------------------------\nFor the lib/nodejs/lib/thrift/json_parse.js:\n\n/*\n json_parse.js\n 2015-05-02\n Public Domain.\n NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n\n*/\n(By Douglas Crockford )\n--------------------------------------------------\n", + "component": "Apache Thrift", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "Unicode-DFS-2015", + "name": "Unicode License Agreement - Data Files and Software (2015)", + "text": "## utf8proc license ##\n\n**utf8proc** is a software package originally developed\nby Jan Behrens and the rest of the Public Software Group, who\ndeserve nearly all of the credit for this library, that is now maintained by the Julia-language developers. Like the original utf8proc,\nwhose copyright and license statements are reproduced below, all new\nwork on the utf8proc library is licensed under the [MIT \"expat\"\nlicense](http://opensource.org/licenses/MIT):\n\n*Copyright © 2014-2019 by Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas Fonseca, and other contributors listed in the git history.*\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n## Original utf8proc license ##\n\n*Copyright (c) 2009, 2013 Public Software Group e. V., Berlin, Germany*\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n## Unicode data license ##\n\nThis software contains data (`utf8proc_data.c`) derived from processing\nthe Unicode data files. The following license applies to that data:\n\n**COPYRIGHT AND PERMISSION NOTICE**\n\n*Copyright (c) 1991-2007 Unicode, Inc. All rights reserved. Distributed\nunder the Terms of Use in http://www.unicode.org/copyright.html.*\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of the Unicode data files and any associated documentation (the \"Data\nFiles\") or Unicode software and any associated documentation (the\n\"Software\") to deal in the Data Files or Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge,\npublish, distribute, and/or sell copies of the Data Files or Software, and\nto permit persons to whom the Data Files or Software are furnished to do\nso, provided that (a) the above copyright notice(s) and this permission\nnotice appear with all copies of the Data Files or Software, (b) both the\nabove copyright notice(s) and this permission notice appear in associated\ndocumentation, and (c) there is clear notice in each modified Data File or\nin the Software as well as in the documentation associated with the Data\nFile(s) or Software that the data or software has been modified.\n\nTHE DATA FILES AND SOFTWARE ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY\nKIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF\nTHIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS\nINCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR\nCONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF\nUSE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THE DATA FILES OR SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder shall\nnot be used in advertising or otherwise to promote the sale, use or other\ndealings in these Data Files or Software without prior written\nauthorization of the copyright holder.\n\nUnicode and the Unicode logo are trademarks of Unicode, Inc., and may be\nregistered in some jurisdictions. All other trademarks and registered\ntrademarks mentioned herein are the property of their respective owners.\n", + "component": "utf8proc", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "The MIT License (MIT)\n\nCopyright (c) 2015 Morwenn \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n", + "component": "vergesort", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "MIT", + "name": "MIT License", + "text": "MIT License\n\nCopyright (c) 2020 YaoYuan \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "component": "yyjson", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] + }, + { + "id": "BSD-3-Clause", + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "text": "BSD License\n\nFor Zstandard software\n\nCopyright (c) 2016-present, Facebook, Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n * Neither the name Facebook nor the names of its contributors may be used to\n endorse or promote products derived from this software without specific\n prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", + "component": "Zstandard", + "used_by": [ + { "crate": { "name": "libduckdb-sys", "version": "1.10506.1" } } + ] } ], "crates": [ { "name": "aes", "version": "0.8.4", "license": "MIT OR Apache-2.0" }, + { "name": "ahash", "version": "0.8.12", "license": "MIT OR Apache-2.0" }, { "name": "aho-corasick", "version": "1.1.5", "license": "Unlicense OR MIT" }, { "name": "allocator-api2", "version": "0.2.21", "license": "MIT OR Apache-2.0" }, + { "name": "arrow", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-arith", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-array", "version": "58.4.0", "license": "Apache-2.0 AND MIT" }, + { "name": "arrow-buffer", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-cast", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-data", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-ord", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-row", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-schema", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-select", "version": "58.4.0", "license": "Apache-2.0" }, + { "name": "arrow-string", "version": "58.4.0", "license": "Apache-2.0" }, { "name": "ashpd", "version": "0.13.13", "license": "MIT" }, { "name": "async-broadcast", "version": "0.7.2", "license": "MIT OR Apache-2.0" }, { "name": "async-recursion", "version": "1.1.1", "license": "MIT OR Apache-2.0" }, @@ -1504,6 +1850,7 @@ { "name": "bytes", "version": "1.12.1", "license": "MIT" }, { "name": "cairo-rs", "version": "0.22.9", "license": "MIT" }, { "name": "cairo-sys-rs", "version": "0.22.9", "license": "MIT" }, + { "name": "cast", "version": "0.3.0", "license": "MIT OR Apache-2.0" }, { "name": "cbc", "version": "0.1.2", "license": "MIT OR Apache-2.0" }, { "name": "cfg-if", "version": "1.0.4", "license": "MIT OR Apache-2.0" }, { "name": "chacha20", "version": "0.10.2", "license": "MIT OR Apache-2.0" }, @@ -1513,6 +1860,7 @@ { "name": "clickhouse-macros", "version": "0.3.0", "license": "MIT OR Apache-2.0" }, { "name": "clickhouse-types", "version": "0.1.3", "license": "MIT OR Apache-2.0" }, { "name": "cmov", "version": "0.5.4", "license": "Apache-2.0 OR MIT" }, + { "name": "comfy-table", "version": "7.1.4", "license": "MIT" }, { "name": "connection-string", "version": "0.2.0", "license": "MIT OR Apache-2.0" }, { "name": "const-oid", "version": "0.9.6", "license": "Apache-2.0 OR MIT" }, { "name": "const-oid", "version": "0.10.2", "license": "Apache-2.0 OR MIT" }, @@ -1525,6 +1873,7 @@ { "name": "crc32fast", "version": "1.5.2", "license": "MIT OR Apache-2.0" }, { "name": "crossbeam-queue", "version": "0.3.14", "license": "MIT OR Apache-2.0" }, { "name": "crossbeam-utils", "version": "0.8.23", "license": "MIT OR Apache-2.0" }, + { "name": "crossterm", "version": "0.28.1", "license": "MIT" }, { "name": "crypto-bigint", "version": "0.7.5", "license": "Apache-2.0 OR MIT" }, { "name": "crypto-common", "version": "0.1.7", "license": "MIT OR Apache-2.0" }, { "name": "crypto-common", "version": "0.2.2", "license": "MIT OR Apache-2.0" }, @@ -1542,6 +1891,7 @@ { "name": "digest", "version": "0.11.3", "license": "MIT OR Apache-2.0" }, { "name": "displaydoc", "version": "0.2.7", "license": "MIT OR Apache-2.0" }, { "name": "dotenvy", "version": "0.15.7", "license": "MIT" }, + { "name": "duckdb", "version": "1.10506.1", "license": "MIT" }, { "name": "either", "version": "1.18.0", "license": "MIT OR Apache-2.0" }, { "name": "encoding_rs", "version": "0.8.41", "license": "(Apache-2.0 OR MIT) AND BSD-3-Clause" }, { "name": "endi", "version": "1.1.1", "license": "MIT" }, @@ -1552,12 +1902,15 @@ { "name": "event-listener", "version": "5.4.2", "license": "Apache-2.0 OR MIT" }, { "name": "event-listener-strategy", "version": "0.5.4", "license": "Apache-2.0 OR MIT" }, { "name": "fallible-iterator", "version": "0.2.0", "license": "MIT OR Apache-2.0" }, + { "name": "fallible-iterator", "version": "0.3.0", "license": "MIT OR Apache-2.0" }, + { "name": "fallible-streaming-iterator", "version": "0.1.9", "license": "MIT OR Apache-2.0" }, { "name": "fastrand", "version": "2.5.0", "license": "Apache-2.0 OR MIT" }, { "name": "field-offset", "version": "0.3.6", "license": "MIT OR Apache-2.0" }, { "name": "fixed_decimal", "version": "0.7.2", "license": "Unicode-3.0" }, { "name": "flagset", "version": "0.4.7", "license": "Apache-2.0" }, { "name": "flate2", "version": "1.1.10", "license": "MIT OR Apache-2.0" }, { "name": "flume", "version": "0.12.0", "license": "Apache-2.0 OR MIT" }, + { "name": "foldhash", "version": "0.1.5", "license": "Zlib" }, { "name": "foldhash", "version": "0.2.0", "license": "Zlib" }, { "name": "form_urlencoded", "version": "1.2.2", "license": "MIT OR Apache-2.0" }, { "name": "fragile", "version": "2.1.0", "license": "Apache-2.0" }, @@ -1598,8 +1951,10 @@ { "name": "gtk4-macros", "version": "0.11.4", "license": "MIT" }, { "name": "gtk4-sys", "version": "0.11.4", "license": "MIT" }, { "name": "half", "version": "2.7.1", "license": "MIT OR Apache-2.0" }, + { "name": "hashbrown", "version": "0.15.5", "license": "MIT OR Apache-2.0" }, { "name": "hashbrown", "version": "0.16.1", "license": "MIT OR Apache-2.0" }, { "name": "hashbrown", "version": "0.17.1", "license": "MIT OR Apache-2.0" }, + { "name": "hashlink", "version": "0.10.0", "license": "MIT OR Apache-2.0" }, { "name": "hashlink", "version": "0.11.1", "license": "MIT OR Apache-2.0" }, { "name": "heck", "version": "0.5.0", "license": "MIT OR Apache-2.0" }, { "name": "hex", "version": "0.4.3", "license": "MIT OR Apache-2.0" }, @@ -1632,13 +1987,21 @@ { "name": "inout", "version": "0.1.4", "license": "MIT OR Apache-2.0" }, { "name": "itoa", "version": "1.0.18", "license": "MIT OR Apache-2.0" }, { "name": "lazy_static", "version": "1.5.0", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-core", "version": "1.0.6", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-parse-float", "version": "1.0.6", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-parse-integer", "version": "1.0.6", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-util", "version": "1.0.7", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-write-float", "version": "1.0.6", "license": "MIT OR Apache-2.0" }, + { "name": "lexical-write-integer", "version": "1.0.6", "license": "MIT OR Apache-2.0" }, { "name": "libadwaita", "version": "0.9.2", "license": "MIT" }, { "name": "libadwaita-sys", "version": "0.9.2", "license": "MIT" }, { "name": "libc", "version": "0.2.189", "license": "MIT OR Apache-2.0" }, + { "name": "libduckdb-sys", "version": "1.10506.1", "license": "Apache-2.0 AND (Apache-2.0 OR GPL-2.0-or-later) AND BSD-2-Clause AND (BSD-2-Clause OR BSL-1.0) AND BSD-3-Clause AND BSL-1.0 AND MIT AND Unicode-DFS-2015 AND Zlib" }, { "name": "libgssapi", "version": "0.11.0", "license": "MIT" }, { "name": "libgssapi-sys", "version": "0.3.4", "license": "MIT" }, { "name": "libm", "version": "0.2.16", "license": "MIT" }, { "name": "libsqlite3-sys", "version": "0.37.0", "license": "MIT" }, + { "name": "linux-raw-sys", "version": "0.4.15", "license": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" }, { "name": "linux-raw-sys", "version": "0.12.1", "license": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" }, { "name": "litemap", "version": "0.8.3", "license": "Unicode-3.0" }, { "name": "locale_config", "version": "0.3.0", "license": "MIT" }, @@ -1697,6 +2060,7 @@ { "name": "relm4-macros", "version": "0.11.0", "license": "Apache-2.0 OR MIT" }, { "name": "rsa", "version": "0.10.0-rc.18", "license": "MIT OR Apache-2.0" }, { "name": "rust_xlsxwriter", "version": "0.99.1", "license": "MIT OR Apache-2.0" }, + { "name": "rustix", "version": "0.38.44", "license": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" }, { "name": "rustix", "version": "1.1.4", "license": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" }, { "name": "rustls", "version": "0.23.46", "license": "Apache-2.0 OR ISC OR MIT" }, { "name": "rustls-native-certs", "version": "0.8.4", "license": "Apache-2.0 OR ISC OR MIT" }, @@ -1741,6 +2105,8 @@ { "name": "sqlx-sqlite", "version": "0.9.0", "license": "MIT OR Apache-2.0" }, { "name": "stable_deref_trait", "version": "1.2.1", "license": "MIT OR Apache-2.0" }, { "name": "stringprep", "version": "0.1.5", "license": "MIT OR Apache-2.0" }, + { "name": "strum", "version": "0.27.2", "license": "MIT" }, + { "name": "strum_macros", "version": "0.27.2", "license": "MIT" }, { "name": "subtle", "version": "2.6.1", "license": "BSD-3-Clause" }, { "name": "syn", "version": "2.0.119", "license": "MIT OR Apache-2.0" }, { "name": "syn", "version": "3.0.5", "license": "MIT OR Apache-2.0" }, @@ -1777,6 +2143,8 @@ { "name": "unicode-ident", "version": "1.0.24", "license": "(MIT OR Apache-2.0) AND Unicode-3.0" }, { "name": "unicode-normalization", "version": "0.1.25", "license": "MIT OR Apache-2.0" }, { "name": "unicode-properties", "version": "0.1.4", "license": "MIT OR Apache-2.0" }, + { "name": "unicode-segmentation", "version": "1.13.3", "license": "MIT OR Apache-2.0" }, + { "name": "unicode-width", "version": "0.2.2", "license": "MIT OR Apache-2.0" }, { "name": "unicode_categories", "version": "0.1.1", "license": "MIT OR Apache-2.0" }, { "name": "untrusted", "version": "0.9.0", "license": "ISC" }, { "name": "url", "version": "2.5.8", "license": "MIT OR Apache-2.0" }, diff --git a/linux/data/resources/resources.gresource.xml b/linux/data/resources/resources.gresource.xml index 3e1a917a9f..bb9e270e19 100644 --- a/linux/data/resources/resources.gresource.xml +++ b/linux/data/resources/resources.gresource.xml @@ -30,7 +30,7 @@ ui/results-pane.ui ui/session-window.ui ui/sql-editor.ui - ui/sqlite-file-row.ui + ui/database-file-row.ui ui/ssh-section.ui ui/structure-tab.ui ui/tls-section.ui diff --git a/linux/data/resources/ui/connect-dialog.ui b/linux/data/resources/ui/connect-dialog.ui index aacf276f6d..65778d135f 100644 --- a/linux/data/resources/ui/connect-dialog.ui +++ b/linux/data/resources/ui/connect-dialog.ui @@ -77,7 +77,7 @@ - + diff --git a/linux/data/resources/ui/sqlite-file-row.ui b/linux/data/resources/ui/database-file-row.ui similarity index 93% rename from linux/data/resources/ui/sqlite-file-row.ui rename to linux/data/resources/ui/database-file-row.ui index 50120b30a0..4fce055010 100644 --- a/linux/data/resources/ui/sqlite-file-row.ui +++ b/linux/data/resources/ui/database-file-row.ui @@ -2,7 +2,7 @@ -