diff --git a/linux/ARCHITECTURE.md b/linux/ARCHITECTURE.md index 7099c2b2ca..e6dd212a48 100644 --- a/linux/ARCHITECTURE.md +++ b/linux/ARCHITECTURE.md @@ -29,6 +29,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 ├── cassandra/ scylla, Cassandra and ScyllaDB @@ -42,7 +43,7 @@ linux/ | `core` | no workspace crate | | `net`, `ssh`, `storage` | `core` | | `drivers/postgres`, `drivers/mysql`, `drivers/mssql`, `drivers/clickhouse`, `drivers/cassandra`, `drivers/redis` | `core`, `net` | -| `drivers/sqlite` | `core` | +| `drivers/sqlite`, `drivers/duckdb` | `core` | | `session` | `core`, `net`, `ssh`, `storage` | | `mcp` | `core`, `session`, `storage` | | `app` | every crate above | @@ -72,6 +73,7 @@ fn build_registry() -> DriverRegistry { let mut r = DriverRegistry::new(); r.register(Arc::new(tablepro_driver_cassandra::CassandraDriver)); 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)); @@ -150,7 +152,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), [Cassandra and ScyllaDB](docs/drivers/cassandra.md), [Redis](docs/drivers/redis.md). An engine without SQL says so through `DriverCapabilities::data_model`, and the workspace lists its keys and opens them in key tabs ([ADR 0021](docs/decisions/0021-engines-without-sql.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), [Cassandra and ScyllaDB](docs/drivers/cassandra.md), [Redis](docs/drivers/redis.md). An engine without SQL says so through `DriverCapabilities::data_model`, and the workspace lists its keys and opens them in key tabs ([ADR 0021](docs/decisions/0021-engines-without-sql.md)). ### SQL scripts @@ -163,6 +165,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 | | CQL | `;` | `BEGIN ... APPLY BATCH`, as cqlsh keeps it, and `$$ ... $$` function bodies | `''` inside a string and no backslash escapes, `//` comments, a line comment that also ends at a carriage return | @@ -233,6 +236,7 @@ Each engine's TLS stack takes trust in its own form, so the driver builds it: | SQL Server | The system store (loaded by tiberius), plus the CA file | No | rustls's message text, because tiberius keeps nothing else | | Cassandra, ScyllaDB | The system store (rustls-platform-verifier), plus the CA file, checked against the server name and not the relay's address (`FixedNameVerifier`) | Yes, with the password login | The rustls error | | SQLite | No TLS | | | +| DuckDB | No TLS | | | PostgreSQL and ClickHouse get their rustls `ClientConfig` from `tablepro_net::tls::build_client_config`, and Cassandra from `build_client_config_checking`, which checks a name it is given in place of the one the client dials. 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..5e5ca2cdba 100644 --- a/linux/CONTRIBUTING.md +++ b/linux/CONTRIBUTING.md @@ -74,7 +74,7 @@ RUST_LOG=debug meson devenv -C _build ./crates/app/tablepro ### Dependencies -`data/licenses` holds the license notices of every crate the app ships, written from `Cargo.lock` by cargo-about. The About dialog embeds `third-party.json`. The packages ship the notices as their license files: `%license` in the RPM, `debian/copyright` in the .deb, and `share/licenses` in the Flatpak. After a change to `Cargo.lock`, regenerate them and commit the result; the Supply chain CI job fails until they match: +`data/licenses` holds the license notices of every crate the app ships, written from `Cargo.lock` by cargo-about. The About dialog embeds `third-party.json`. The packages ship the notices as their license files: `%license` in the RPM, `debian/copyright` in the .deb, and `share/licenses` in the Flatpak. `NOTICE` gathers the crates' NOTICE files, which the Apache License 2.0 asks to ship; the .deb installs it as a doc. After a change to `Cargo.lock`, regenerate them and commit the result; the Supply chain CI job fails until they match: ```bash cargo install --locked cargo-about@0.9.2 --features cli @@ -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 7ecdb33068..77d2bbcc1f 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" @@ -59,6 +79,169 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" +[[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" @@ -122,7 +305,7 @@ dependencies = [ "libc", "portable-atomic", "rustc-hash", - "rustix", + "rustix 1.1.4", "tokio", "tokio-stream", "xattr", @@ -572,6 +755,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" @@ -618,6 +807,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" @@ -742,6 +937,17 @@ dependencies = [ "tokio-util", ] +[[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" @@ -760,6 +966,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" @@ -845,6 +1071,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" @@ -853,9 +1101,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" dependencies = [ "cpubits", "ctutils", @@ -917,9 +1165,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", ] @@ -1150,6 +1398,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" @@ -1226,7 +1491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1265,6 +1530,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" @@ -1295,6 +1572,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" @@ -1324,6 +1611,8 @@ version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ + "crc32fast", + "miniz_oxide", "zlib-rs", ] @@ -1345,6 +1634,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" @@ -1566,7 +1861,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", ] @@ -1654,7 +1949,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps 9.0.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1879,6 +2174,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -1894,6 +2190,15 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[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" @@ -1902,7 +2207,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1913,7 +2218,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]] @@ -2531,6 +2845,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" @@ -2567,6 +2938,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" @@ -2624,6 +3012,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" @@ -2744,6 +3138,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" @@ -2782,6 +3186,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" @@ -2894,6 +3310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3218,7 +3635,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", @@ -3235,7 +3652,7 @@ checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" dependencies = [ "bytes", "chrono", - "fallible-iterator", + "fallible-iterator 0.2.0", "postgres-protocol", ] @@ -3636,7 +4053,7 @@ checksum = "34f2d2cda7223168a14fb86bc7411dbc3379bd7d49749ecc53b91ed98585b6b0" dependencies = [ "chrono", "tempfile", - "zip", + "zip 8.6.0", ] [[package]] @@ -3663,6 +4080,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" @@ -3672,8 +4102,8 @@ dependencies = [ "bitflags 2.13.2", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -3685,6 +4115,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -3730,7 +4161,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4351,7 +4782,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink", + "hashlink 0.11.1", "indexmap 2.14.2", "log", "memchr", @@ -4561,6 +4992,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" @@ -4652,7 +5104,7 @@ dependencies = [ "log", "proptest", "relm4", - "rustix", + "rustix 1.1.4", "secrecy", "serde", "serde_json", @@ -4661,6 +5113,7 @@ dependencies = [ "tablepro-core", "tablepro-driver-cassandra", "tablepro-driver-clickhouse", + "tablepro-driver-duckdb", "tablepro-driver-mssql", "tablepro-driver-mysql", "tablepro-driver-postgres", @@ -4704,7 +5157,7 @@ dependencies = [ "tokio", "tokio-util", "uuid", - "zip", + "zip 8.6.0", ] [[package]] @@ -4715,7 +5168,7 @@ dependencies = [ "chrono", "futures-util", "num-bigint", - "rustix", + "rustix 1.1.4", "rustls", "rustls-pki-types", "scylla", @@ -4758,6 +5211,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" @@ -4808,7 +5279,7 @@ dependencies = [ "bytes", "chrono", "deadpool-postgres", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-util", "postgres-protocol", "postgres-types", @@ -4948,7 +5419,7 @@ version = "0.0.0" dependencies = [ "async-trait", "futures", - "rustix", + "rustix 1.1.4", "secrecy", "tablepro-core", "tablepro-test-fixtures", @@ -5016,6 +5487,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" @@ -5037,7 +5519,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.52.0", ] @@ -5186,6 +5668,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" @@ -5261,7 +5752,7 @@ dependencies = [ "async-trait", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-channel", "futures-util", "log", @@ -5594,6 +6085,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" @@ -5625,6 +6128,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", + "webpki-roots", ] [[package]] @@ -5879,7 +6383,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6066,7 +6570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a8885a854a8bfdf87a301e53e41b17c5f8f33639903131338b997b1eb614f44" dependencies = [ "gethostname", - "rustix", + "rustix 1.1.4", "x11rb-protocol", ] @@ -6113,7 +6617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -6171,7 +6675,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.4", "serde", "serde_repr", "tokio", @@ -6330,6 +6834,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 51be182893..4b91cd57d1 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/test-support", "crates/drivers/cassandra", "crates/drivers/clickhouse", + "crates/drivers/duckdb", "crates/drivers/mssql", "crates/drivers/mysql", "crates/drivers/postgres", @@ -29,6 +30,7 @@ publish = false tablepro-core = { path = "crates/core" } tablepro-driver-cassandra = { path = "crates/drivers/cassandra" } 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" } @@ -85,6 +87,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/0025-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"] } @@ -136,6 +142,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 6037870359..da2c06dc9d 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 seven engines: PostgreSQL, MySQL and MariaDB, SQL Server, ClickHouse, Cassandra and ScyllaDB, SQLite and Redis. 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 eight engines: PostgreSQL, MySQL and MariaDB, SQL Server, ClickHouse, Cassandra and ScyllaDB, SQLite, DuckDB and Redis. 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 @@ -20,6 +20,7 @@ TablePro supports the server releases their vendors still support. CI runs each | Apache Cassandra | 4.0 and 5.0 | 3.11 and older, past end of life. 6.0 until it is released | | ScyllaDB | 2025.1 and 2026.1 | Feature releases between long-term ones, such as 2026.3. Enterprise 2024.1 and older, past end of life | | 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 0025](docs/decisions/0025-bundled-duckdb.md)) | files a newer DuckDB wrote in a storage format 1.5.6 cannot read | | Redis | 7.4 and 8.10 | 7.2 and older, past end of life. Cluster and Sentinel deployments. Valkey answers the same commands and is not tested | - PostgreSQL 14 reaches end of life on 2026-11-12. @@ -50,7 +51,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. With the [MCP server](#mcp-server) on, it also listens on `127.0.0.1` for MCP clients on this computer, which send what they read wherever their own settings say. 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. With the [MCP server](#mcp-server) on, it also listens on `127.0.0.1` for MCP clients on this computer, which send what they read wherever their own settings say. Crash reports stay on your disk until you attach one to a bug report. Updates come from apt or dnf. ## Stack @@ -60,7 +61,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), the official `clickhouse` crate and scylla 1.9 (Cassandra and ScyllaDB), 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, scylla 1.9 (Cassandra and ScyllaDB) 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)) | @@ -86,7 +87,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 @@ -145,6 +146,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 @@ -225,19 +230,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. ## Vim keybindings @@ -304,9 +312,8 @@ added, changed or removed takes effect without a restart: - `languages` says where a snippet shows: `sql` on every SQL connection and in a tab with no connection, or one engine: `postgres`, `mysql` (MySQL and MariaDB), `sqlite`, `mssql`, `clickhouse`, `cassandra` - (Cassandra and ScyllaDB) or `redis`, whose - editor offers its own snippets and no SQL ones. Separate several with - `;`. + (Cassandra and ScyllaDB), `duckdb` or `redis`, whose editor offers its + own snippets and no SQL ones. Separate several with `;`. - A snippet for the engine replaces a `sql` one with the same trigger, and either replaces the built-in one. - A file that does not parse is skipped, and GtkSourceView logs its name. diff --git a/linux/ROADMAP.md b/linux/ROADMAP.md index 717e01c4d3..916d8b31c9 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 seven 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), [Cassandra and ScyllaDB](docs/drivers/cassandra.md), [SQLite](docs/drivers/sqlite.md) and [Redis](docs/drivers/redis.md). +The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor.md)) and connects to eight 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), [Cassandra and ScyllaDB](docs/drivers/cassandra.md), [SQLite](docs/drivers/sqlite.md), [DuckDB](docs/drivers/duckdb.md) and [Redis](docs/drivers/redis.md). ## Implemented @@ -17,8 +17,8 @@ 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, tags and icons included. On import, an entry with the same id and the same settings updates its name, colour, group, tags and icon; any other is added as a new connection. Tags are matched by id, then by name, so a second import adds none | | 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, SQL Server relies on the login, with the app's own reads in a rolled-back transaction, and Cassandra and ScyllaDB rely on the login. 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 | +| 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, SQL Server relies on the login, with the app's own reads in a rolled-back transaction, and Cassandra and ScyllaDB rely on the login. 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 | +| Database files | SQLite and DuckDB: choose a file, create a new one, open one from Files or a terminal, where the file's header picks the engine, 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 | | Cancel on the server | Stopping a query stops the statement on the server (in process on SQLite); a ClickHouse mutation already running in a save is left to finish. CQL has no stop, so on Cassandra and ScyllaDB the app stops waiting and the server runs the statement to its own timeout | @@ -114,7 +114,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor | Window per session | A window holds one connection; another connection opens in a window of its own, and one already open brings its window forward ([ADR 0012](docs/decisions/0012-window-per-session.md)) | | One instance | A second launch presents the running app; New Window (Ctrl+Shift+N) opens a picker | | Tabs | `AdwTabView` with the tab overview and split header bars, adapting down to 360×294 | -| Session restore | The windows of saved connections and SQLite files open at quit open again at launch (Restore Session, on by default), also after New Window or a quit during the restore | +| Session restore | The windows of saved connections and database files open at quit open again at launch (Restore Session, on by default), also after New Window or a quit during the restore | | Closing safely | Unsaved edits, a save in flight or of unknown outcome, an open editor transaction, or a running export ask before a window closes or the app quits; logout waits while any of those holds, and suspend while a save runs or an export writes | | Notifications | Work that finishes in a tab you are not looking at sends a notification | | Preferences | Restore session, page size, the bulk delete confirmation, query history, editor font and scheme, Vim keybindings, query timeout, the MCP server | @@ -179,7 +179,6 @@ Each one is a crate under `crates/drivers` ([adding-drivers.md](docs/adding-driv | Engine | Client | Condition | |---|---|---| | 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 | | Oracle | none | Blocked. The oracle crate has one maintainer, no release since 2025-01-02, and needs the proprietary Oracle client | Microsoft's mssql-tds is checked again each release as a SQL Server client. Today its TLS on Linux goes through OpenSSL and it takes no outside pull requests, so tiberius stays. @@ -197,7 +196,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, Cassandra, ScyllaDB 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, Cassandra, ScyllaDB 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, Cassandra, ScyllaDB, 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 c85d31b3b7..6eb4acff9a 100644 --- a/linux/about.toml +++ b/linux/about.toml @@ -186,6 +186,12 @@ 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" # The LICENSE opens with a note on the project's move from MIT to # Apache-2.0, then holds both texts: code whose authors have not agreed to # the move stays under MIT, so the notice carries both. 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 7ac0e23635..9665d7d28d 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/* @@ -822,6 +851,103 @@ License: Apache-2.0 or Expat or Zlib 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 @@ -884,6 +1010,12 @@ Files: vendor/async-lock-3.4.2/* 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/* @@ -913,7 +1045,9 @@ Files: vendor/async-lock-3.4.2/* vendor/typed-path-0.12.3/* Copyright: the authors of async-lock, async-trait, clickhouse, clickhouse-macros, clickhouse-types, displaydoc, dyn-clone, event-listener, - event-listener-strategy, fastrand, itoa, once_cell, ordered-stream, + event-listener-strategy, fastrand, itoa, lexical-core, + lexical-parse-float, lexical-parse-integer, lexical-util, + lexical-write-float, lexical-write-integer, once_cell, ordered-stream, parking, pastey, pin-project-lite, proc-macro-crate, proc-macro2, quote, ref-cast, ref-cast-impl, relm4, relm4-macros, rustversion, serde, serde_bytes, serde_core, serde_derive, serde_derive_internals, @@ -1816,6 +1950,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 @@ -2018,6 +2181,31 @@ License: Expat OUT OF OR 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/* @@ -2274,7 +2462,32 @@ License: Apache-2.0 or Expat IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Files: vendor/crypto-bigint-0.7.5/* +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.6/* vendor/spki-0.8.0/* Copyright: 2021-2026 The RustCrypto Project Developers License: Apache-2.0 or Expat @@ -2373,7 +2586,7 @@ License: Apache-2.0 or Expat . 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/ctutils-0.4.4/* +Files: vendor/ctutils-0.4.5/* Copyright: 2025-2026 The RustCrypto Project Developers License: Apache-2.0 or Expat Copyright (c) 2025-2026 The RustCrypto Project Developers @@ -2596,6 +2809,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/* vendor/itertools-0.15.0/* Copyright: 2015 @@ -2884,6 +3108,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 @@ -2906,6 +3131,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 @@ -3003,38 +3251,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 @@ -3064,7 +3280,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 @@ -3322,6 +3539,7 @@ License: Apache-2.0 or Expat SOFTWARE. Files: vendor/hashbrown-0.14.5/* + vendor/hashbrown-0.15.5/* vendor/hashbrown-0.16.1/* vendor/hashbrown-0.17.1/* Copyright: 2016 Amanieu d'Antras @@ -3352,7 +3570,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) @@ -3386,6 +3605,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 @@ -3859,13 +4080,49 @@ 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 + 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 @@ -3874,9 +4131,838 @@ 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. + . + . + =============================================================================== + . + . + On Debian systems, the complete text of the GNU General Public License + version 2 can be found in "/usr/share/common-licenses/GPL-2". + . + 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, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -4004,6 +5090,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 @@ -5436,6 +6524,32 @@ License: 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/debian/docs b/linux/build-aux/debian/docs new file mode 100644 index 0000000000..bbe345efb0 --- /dev/null +++ b/linux/build-aux/debian/docs @@ -0,0 +1 @@ +data/licenses/NOTICE diff --git a/linux/build-aux/debian/rules b/linux/build-aux/debian/rules index 384c395658..2138013052 100755 --- a/linux/build-aux/debian/rules +++ b/linux/build-aux/debian/rules @@ -10,10 +10,11 @@ export PATH := /usr/lib/rust-1.97/bin:$(PATH) # component tarball, which dpkg-source unpacks into vendor/. export CARGO_NET_OFFLINE = true -# Cargo starts one rustc per CPU unless told otherwise, and the largest -# optimized crates take up to 3 GB each, so the jobs follow the memory as -# well as DEB_BUILD_OPTIONS: one per 2 GiB. -memory_jobs := $(shell awk '/^MemAvailable:/ { jobs = int($$2 / 2097152); print (jobs < 1 ? 1 : jobs) }' /proc/meminfo) +# Cargo starts one rustc or DuckDB g++ per CPU unless told otherwise, and +# the largest optimized crates and DuckDB translation units take up to 3 GB +# each, so the jobs follow the memory as well as DEB_BUILD_OPTIONS: one per +# 3 GiB. +memory_jobs := $(shell awk '/^MemAvailable:/ { jobs = int($$2 / 3145728); print (jobs < 1 ? 1 : jobs) }' /proc/meminfo) parallel_jobs := $(or $(DEB_BUILD_OPTION_PARALLEL),1) export CARGO_BUILD_JOBS := $(shell if [ $(memory_jobs) -lt $(parallel_jobs) ]; then echo $(memory_jobs); else echo $(parallel_jobs); fi) @@ -24,7 +25,10 @@ export CARGO_PROFILE_RELEASE_LTO = thin # The release profile carries no debug info, which leaves the dbgsym # package empty; line tables are enough to symbolize a backtrace. export CARGO_PROFILE_RELEASE_DEBUG = line-tables-only -export DEB_BUILD_MAINT_OPTIONS = hardening=+all +# Ubuntu compiles C and C++ into fat LTO objects by default, which double +# what DuckDB's C++ build writes and the memory each of its translation +# units takes; rustc links without -flto, so their GIMPLE half goes unused. +export DEB_BUILD_MAINT_OPTIONS = hardening=+all optimize=-lto %: dh $@ --buildsystem=meson 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/crate_notices.py b/linux/build-aux/packaging/crate_notices.py new file mode 100644 index 0000000000..68286046fe --- /dev/null +++ b/linux/build-aux/packaging/crate_notices.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# Writes data/licenses/NOTICE from the NOTICE files of the crates +# third-party.json lists. The Apache License 2.0 asks for a work's NOTICE +# to ship with what is built from it, and cargo-about reads license files +# only. + +import argparse +import json +import sys +import textwrap +from pathlib import Path + +NAMES = ("NOTICE", "NOTICE.txt", "NOTICE.md") +HEADER = """\ +The NOTICE files of the crates linked into TablePro, as the Apache License +2.0 asks. build-aux/update-third-party-licenses.sh writes this file. +""" + + +class NoticeError(Exception): + pass + + +def crate_directories(metadata): + return { + (package["name"], package["version"]): Path(package["manifest_path"]).parent + for package in metadata["packages"] + } + + +def collect(crates, directories): + notices = {} + for crate in crates: + key = (crate["name"], crate["version"]) + if key not in directories: + raise NoticeError(f"cargo metadata has no {key[0]} {key[1]}") + for name in NAMES: + path = directories[key] / name + if not path.is_file(): + continue + text = path.read_text(encoding="utf-8").strip("\n") + if text.strip(): + notices.setdefault(text, []).append(f"{key[0]} {key[1]}") + return notices + + +def render(notices): + sections = [HEADER] + for text, crates in sorted(notices.items(), key=lambda item: item[1][0]): + used_by = textwrap.wrap("Used by: " + ", ".join(crates), width=72, break_on_hyphens=False) + sections.append("\n".join(used_by) + "\n\n" + text + "\n") + return "\n".join(sections) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Write the NOTICE files of the linked crates to one file.") + parser.add_argument("--licenses", required=True) + parser.add_argument("--metadata", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args(argv) + + try: + crates = json.loads(Path(args.licenses).read_text(encoding="utf-8"))["crates"] + metadata = json.loads(Path(args.metadata).read_text(encoding="utf-8")) + text = render(collect(crates, crate_directories(metadata))) + except (OSError, KeyError, ValueError, NoticeError) as error: + print(f"crate_notices.py: {error}", file=sys.stderr) + return 1 + + Path(args.output).write_text(text, 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..6a31c1ca4b 100644 --- a/linux/build-aux/packaging/license_metadata.py +++ b/linux/build-aux/packaging/license_metadata.py @@ -27,10 +27,20 @@ "On Debian systems, the complete text of the Apache License, Version 2.0", 'can be found in "/usr/share/common-licenses/Apache-2.0".', ] +# The same holds for the GPL 2 terms, whose FSF address lintian also +# reports as out of date. The cut runs to the end of its appendix, which +# carries the address too. +GPL2_TITLE = re.compile(r"^\s*GNU GENERAL PUBLIC LICENSE\s*$") +GPL2_VERSION = re.compile(r"^\s*Version 2, June 1991\s*$") +GPL2_LAST = "Public License instead of this License." +GPL2_POINTER = [ + "On Debian systems, the complete text of the GNU General Public License", + 'version 2 can be found in "/usr/share/common-licenses/GPL-2".', +] 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" @@ -214,10 +224,13 @@ def crate_texts(licenses, crates): def debian_text(text): - lines = text.split("\n") - if APACHE_TERMS not in text: + return without_gpl2(without_apache(text.split("\n"))) + + +def without_apache(lines): + terms = next((i for i, line in enumerate(lines) if APACHE_TERMS in line), None) + if terms is None: return lines - terms = next(i for i, line in enumerate(lines) if APACHE_TERMS in line) titles = [i for i in range(terms) if APACHE_TITLE.match(lines[i])] ends = [i for i in range(terms, len(lines)) if APACHE_END in lines[i]] if not titles or not ends: @@ -225,6 +238,21 @@ def debian_text(text): return lines[: titles[-1]] + APACHE_POINTER + lines[ends[0] + 1 :] +def without_gpl2(lines): + # The LGPL's title differs, so its text stays. + content = [i for i, line in enumerate(lines) if line.strip()] + start = next( + (i for i, n in zip(content, content[1:]) if GPL2_TITLE.match(lines[i]) and GPL2_VERSION.match(lines[n])), + None, + ) + if start is None: + return lines + end = next((i for i in range(start, len(lines)) if GPL2_LAST in lines[i]), None) + if end is None: + raise MetadataError("a GPL 2 text has no last line to cut at") + return lines[:start] + GPL2_POINTER + lines[end + 1 :] + + def notices(texts): found = [] for text in texts: @@ -318,7 +346,8 @@ def render_copyright(licenses, crates, upstream_text, metadata_license): group = sorted(group) paragraphs.append( field("Files", [f"vendor/{name}-{version}/*" for name, version in group]) - + field("Copyright", notices(texts) or unnamed_holders(group)) + # The cut license texts hold the FSF's notice, not a holder's. + + field("Copyright", notices(["\n".join(debian_text(text)) for text in texts]) or unnamed_holders(group)) + field("License", [synopsis] + text_block(texts)) ) diff --git a/linux/build-aux/rpm/tablepro.spec b/linux/build-aux/rpm/tablepro.spec index d9e445d280..b841deffcd 100644 --- a/linux/build-aux/rpm/tablepro.spec +++ b/linux/build-aux/rpm/tablepro.spec @@ -6,6 +6,10 @@ # module, and its unit test build passed 7 GB that way. Cargo's release # default of 16 splits the work, and thin LTO still optimizes across it. %global rustflags_codegen_units 16 +# Fedora compiles C and C++ into fat LTO objects, which double what +# DuckDB's C++ build writes and the memory each of its translation units +# takes; rustc links without -flto, so their GIMPLE half goes unused. +%global _lto_cflags %{nil} %bcond check 1 @@ -23,11 +27,14 @@ 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 @@ -35,6 +42,7 @@ License: %{shrink: MIT AND (MIT OR Unlicense) AND Unicode-3.0 AND + Unicode-DFS-2015 AND Zlib } URL: https://tablepro.app @@ -47,6 +55,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 @@ -93,9 +103,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 @@ -141,7 +151,7 @@ export CARGO_BUILD_JOBS=%{_smp_build_ncpus} %endif %files -f %{name}.lang -%license LICENSE data/licenses/THIRD-PARTY-NOTICE.md +%license LICENSE data/licenses/THIRD-PARTY-NOTICE.md data/licenses/NOTICE %license LICENSE.dependencies cargo-vendor.txt %doc CHANGELOG.md %{_bindir}/tablepro 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_crate_notices.py b/linux/build-aux/tests/test_crate_notices.py new file mode 100644 index 0000000000..c22102e370 --- /dev/null +++ b/linux/build-aux/tests/test_crate_notices.py @@ -0,0 +1,78 @@ +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 crate_notices # noqa: E402 + +ARROW = "Apache Arrow\nCopyright 2016-2026 The Apache Software Foundation\n" + + +class CrateNoticesTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + + def tearDown(self): + self.directory.cleanup() + + def package(self, name, version, files): + crate = self.root / f"{name}-{version}" + crate.mkdir() + (crate / "Cargo.toml").write_text("[package]\n", encoding="utf-8") + for file_name, text in files.items(): + (crate / file_name).write_text(text, encoding="utf-8") + return {"name": name, "version": version, "manifest_path": str(crate / "Cargo.toml")} + + def run_main(self, crates, packages): + licenses = self.root / "third-party.json" + metadata = self.root / "metadata.json" + output = self.root / "NOTICE" + licenses.write_text(json.dumps({"licenses": [], "crates": crates}), encoding="utf-8") + metadata.write_text(json.dumps({"packages": packages}), encoding="utf-8") + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + code = crate_notices.main( + ["--licenses", str(licenses), "--metadata", str(metadata), "--output", str(output)] + ) + return code, output.read_text(encoding="utf-8") if output.exists() else None, stderr.getvalue() + + def test_crates_sharing_a_notice_are_listed_once_above_it(self): + packages = [ + self.package("arrow", "58.4.0", {"NOTICE.txt": ARROW}), + self.package("arrow-array", "58.4.0", {"NOTICE.txt": ARROW}), + self.package("serde", "1.0.0", {}), + ] + crates = [{"name": p["name"], "version": p["version"], "license": "Apache-2.0"} for p in packages] + + code, written, _ = self.run_main(crates, packages) + + self.assertEqual(code, 0) + self.assertTrue(written.startswith(crate_notices.HEADER)) + self.assertEqual(written.count("Apache Arrow"), 1) + self.assertIn("Used by: arrow 58.4.0, arrow-array 58.4.0\n\n" + ARROW, written) + self.assertNotIn("serde", written) + + def test_a_crate_cargo_metadata_does_not_list_is_refused(self): + code, written, stderr = self.run_main([{"name": "gone", "version": "1.0.0", "license": "MIT"}], []) + + self.assertEqual(code, 1) + self.assertIsNone(written) + self.assertIn("gone 1.0.0", stderr) + + def test_a_package_left_out_of_third_party_json_is_not_read(self): + packages = [self.package("dev-only", "1.0.0", {"NOTICE": "Dev Only\n"})] + + code, written, _ = self.run_main([], packages) + + self.assertEqual(code, 0) + self.assertEqual(written, crate_notices.HEADER) + + +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..86d48b8508 100644 --- a/linux/build-aux/tests/test_license_metadata.py +++ b/linux/build-aux/tests/test_license_metadata.py @@ -26,6 +26,34 @@ Copyright [yyyy] [name of copyright owner] """ +GPL2 = """ + 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 + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. +""" + +LGPL21 = """ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +""" + MIT_ALICE = """Copyright (c) 2019 Alice Permission is hereby granted, free of charge, to any person obtaining a copy. @@ -280,6 +308,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/*") @@ -304,6 +339,33 @@ def test_an_apache_text_with_no_end_line_is_refused(self): with self.assertRaises(license_metadata.MetadataError): self.render([entry("Apache-2.0", broken, crate("ryu", "1.0.0"))]) + def test_the_gpl2_terms_and_their_fsf_address_point_at_common_licenses(self): + dual = "Copyright The Mbed TLS Contributors\n\nApache-2.0 OR GPL-2.0-or-later.\n" + APACHE + "\n=====\n" + GPL2 + licenses = [entry("Apache-2.0", dual, crate("mbedtls", "1.0.0"))] + rendered = self.render(licenses, declared(licenses, mbedtls="Apache-2.0 OR GPL-2.0-or-later")) + paragraph = files_paragraph(rendered, "vendor/mbedtls-1.0.0/*") + body = "\n".join(paragraph["License"][1:]) + + self.assertNotIn("TERMS AND CONDITIONS FOR COPYING", rendered) + self.assertNotIn("02110-1301", rendered) + self.assertIn("/usr/share/common-licenses/Apache-2.0", body) + self.assertIn("/usr/share/common-licenses/GPL-2", body) + self.assertTrue(body.rstrip().endswith('"/usr/share/common-licenses/GPL-2".')) + self.assertEqual(paragraph["Copyright"], ["The Mbed TLS Contributors"]) + + def test_an_lgpl_text_is_kept(self): + licenses = [entry("LGPL-2.1-or-later", LGPL21, crate("a", "1.0.0"))] + body = "\n".join(files_paragraph(self.render(licenses), "vendor/a-1.0.0/*")["License"][1:]) + + self.assertIn("TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION", body) + self.assertNotIn("common-licenses", body) + + def test_a_gpl2_text_with_no_last_line_is_refused(self): + broken = GPL2.replace("Public License instead of this License.", "") + + with self.assertRaises(license_metadata.MetadataError): + self.render([entry("GPL-2.0-or-later", broken, crate("a", "1.0.0"))]) + def test_blank_lines_and_tabs_are_deb822_safe(self): licenses = [entry("MIT", "Copyright 2020 Bob\n\n\tIndented\fline\n\n", crate("a", "1.0.0"))] rendered = self.render(licenses) diff --git a/linux/build-aux/update-third-party-licenses.sh b/linux/build-aux/update-third-party-licenses.sh index e12fbb491f..3dab50c8ed 100755 --- a/linux/build-aux/update-third-party-licenses.sh +++ b/linux/build-aux/update-third-party-licenses.sh @@ -33,7 +33,8 @@ generate() { # A crate with no license file cargo-about can read gets the SPDX template # text, which names no copyright holder, and --fail lets that through. fallbacks="$(mktemp)" -trap 'rm -f "$fallbacks"' EXIT +metadata="$(mktemp)" +trap 'rm -f "$fallbacks" "$metadata"' EXIT generate "$root/build-aux/about/fallbacks.hbs" "$fallbacks" if grep -q '[^[:space:]]' "$fallbacks"; then echo "update-third-party-licenses.sh: no license file read for these crates; add or fix their [.clarify] entry in about.toml:" >&2 @@ -45,6 +46,21 @@ 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" + +# The NOTICE files the Apache License 2.0 asks to ship, which cargo-about +# does not read. +cargo metadata --locked --format-version 1 --manifest-path "$root/Cargo.toml" > "$metadata" +python3 "$root/build-aux/packaging/crate_notices.py" \ + --licenses "$root/data/licenses/third-party.json" \ + --metadata "$metadata" \ + --output "$root/data/licenses/NOTICE" + python3 "$root/build-aux/packaging/license_metadata.py" write \ --licenses "$root/data/licenses/third-party.json" \ --license-file "$root/LICENSE" \ diff --git a/linux/changelog.d/3806.added.md b/linux/changelog.d/3806.added.md new file mode 100644 index 0000000000..92e36a89a1 --- /dev/null +++ b/linux/changelog.d/3806.added.md @@ -0,0 +1 @@ +DuckDB files open from the connect dialog, Files or a terminal, with tables to browse, edit and export. diff --git a/linux/crates/app/Cargo.toml b/linux/crates/app/Cargo.toml index 1df29a42e6..26b91d836f 100644 --- a/linux/crates/app/Cargo.toml +++ b/linux/crates/app/Cargo.toml @@ -61,6 +61,7 @@ kerberos = ["tablepro-driver-mssql/kerberos"] tablepro-core.workspace = true tablepro-driver-cassandra.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 b91fc21c24..408135d88a 100644 --- a/linux/crates/app/src/application/app_services.rs +++ b/linux/crates/app/src/application/app_services.rs @@ -88,12 +88,13 @@ 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()); let snippets = SnippetLibrary::new(storage.paths().snippets_dir()); 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 de1e6abf87..11343b7632 100644 --- a/linux/crates/app/src/lib.rs +++ b/linux/crates/app/src/lib.rs @@ -70,6 +70,7 @@ fn build_registry() -> DriverRegistry { let mut r = DriverRegistry::new(); r.register(Arc::new(tablepro_driver_cassandra::CassandraDriver)); 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)); @@ -94,6 +95,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 b2385e5d92..399c46e161 100644 --- a/linux/crates/app/src/logging.rs +++ b/linux/crates/app/src/logging.rs @@ -17,8 +17,10 @@ use crate::config::Profile; /// clickhouse crate exceptions that quote the query, scylla the server's /// error messages, which quote the statement, 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. +/// 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", @@ -28,6 +30,7 @@ const SILENCED_LIBRARIES: &[&str] = &[ "scylla", "redis", "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 8a43241169..fd4cbced9b 100644 --- a/linux/crates/app/src/services/file_resolution.rs +++ b/linux/crates/app/src/services/file_resolution.rs @@ -1,9 +1,10 @@ //! 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::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 +83,121 @@ 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, and no +/// engine opens a file until the read is done (`opening_file_blocking`). +pub fn read_header_blocking(path: &Path, len: usize) -> Result, DriverError> { + let _opening = tablepro_core::opening_file_blocking(); + 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)?; + // A FIFO would hold the read, and every engine's open behind it, + // until something wrote to it. + if !metadata.is_file() { + return Err(DriverError::NotADatabase { + path: path.to_path_buf(), + }); + } + if tablepro_core::held_by_this_process(path) { + 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) +} + #[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 FIFO would wait for a writer, with every engine's open + /// waiting behind it. + #[test] + fn a_fifo_is_not_read() { + let directory = tempfile::tempdir().expect("tempdir"); + let path = directory.path().join("pipe.db"); + let made = std::process::Command::new("mkfifo") + .arg(&path) + .status() + .expect("mkfifo"); + assert!(made.success()); + + assert_eq!(read_header_blocking(&path, 16), Err(DriverError::NotADatabase { path })); + } + + /// An engine opening a file holds no descriptor on it yet, so the + /// read waits until it does. + #[test] + fn a_header_read_waits_for_an_engine_opening_a_file() { + let directory = tempfile::tempdir().expect("tempdir"); + let path = directory.path().join("opening.db"); + std::fs::write(&path, b"SQLite format 3\0").expect("seed"); + let opening = tablepro_core::opening_file_blocking(); + + let reading = std::thread::spawn(move || read_header_blocking(&path, 16)); + std::thread::sleep(std::time::Duration::from_millis(100)); + let waited = !reading.is_finished(); + drop(opening); + + assert!(waited, "the header was read while an engine was opening a file"); + assert_eq!( + reading.join().expect("the read").expect("the header"), + b"SQLite format 3\0" + ); + } + + /// 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 8c391b2c6a..e2f34527e4 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, ConnectionTag, 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>, @@ -39,11 +42,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([])), tags: RefCell::new(Arc::from([])), state: RefCell::new(ConnectionListState::Loading), @@ -128,11 +132,10 @@ impl SavedConnectionList { self.connections.replace(sorted.clone()); self.tags.replace(Arc::from(tags)); if regrouped || retagged || 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 @@ -147,9 +150,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| { @@ -161,7 +178,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; } @@ -239,7 +256,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"); @@ -256,7 +273,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| { @@ -286,7 +303,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); @@ -309,7 +326,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); @@ -327,7 +344,7 @@ mod tests { fn a_changed_tag_replaces_every_object_and_the_tags_read_by_name() { 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")); let name = |name: &str| tablepro_storage::ConnectionTagName::new(name).expect("a tag name"); let zeta = ConnectionTag::new(name("zeta"), tablepro_storage::ConnectionColor::Red); @@ -357,7 +374,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 3d716f1252..6f502692e4 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, saved.username.as_str()) { (AuthMode::Kerberos, _) | (AuthMode::Password, "") => { @@ -78,15 +82,15 @@ mod tests { #[test] fn a_row_without_a_username_has_no_separator_to_dangle() { assert_eq!( - subtitle_for(&saved("", AuthMode::Kerberos), None), + subtitle_for(&saved("", AuthMode::Kerberos), false, None), "mssql · sql.corp.example:1433" ); let mut redis = saved("", AuthMode::Password); redis.driver_id = "redis".into(); redis.port = 6379; - assert_eq!(subtitle_for(&redis, None), "redis · sql.corp.example:6379"); + assert_eq!(subtitle_for(&redis, false, None), "redis · sql.corp.example:6379"); assert_eq!( - subtitle_for(&saved("sa", AuthMode::Password), None), + subtitle_for(&saved("sa", AuthMode::Password), false, None), "mssql · sa@sql.corp.example:1433" ); } @@ -98,12 +102,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] @@ -112,7 +129,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 d35b45dd78..36da2d12fe 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] @@ -113,7 +113,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 584700cbfb..7d6ad51e81 100644 --- a/linux/crates/app/src/ui/connect_dialog/mod.rs +++ b/linux/crates/app/src/ui/connect_dialog/mod.rs @@ -6,12 +6,12 @@ mod appearance; 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; @@ -39,11 +39,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; @@ -492,6 +492,7 @@ impl ConnectDialog { crate::i18n::gettext("Username (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() }; @@ -918,7 +919,12 @@ pub fn remedy_field(remedy: TlsRemedy) -> FormField { /// still has something to read. static NO_DRIVER: DriverCapabilities = DriverCapabilities { data_model: tablepro_core::DataModel::Relational, - 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: &[], username_required: true, tls: None, diff --git a/linux/crates/app/src/ui/connect_dialog/tests.rs b/linux/crates/app/src/ui/connect_dialog/tests.rs index 991ac917c9..2b7800baf1 100644 --- a/linux/crates/app/src/ui/connect_dialog/tests.rs +++ b/linux/crates/app/src/ui/connect_dialog/tests.rs @@ -51,6 +51,7 @@ fn registry() -> Arc { let mut registry = DriverRegistry::new(); registry.register(Arc::new(tablepro_driver_cassandra::CassandraDriver)); 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/diagram/engines.rs b/linux/crates/app/src/ui/diagram/engines.rs index 71369814e2..66c85539bb 100644 --- a/linux/crates/app/src/ui/diagram/engines.rs +++ b/linux/crates/app/src/ui/diagram/engines.rs @@ -1,5 +1,5 @@ //! The diagram read from real servers: a schema of 50 tables joined by -//! foreign keys, on PostgreSQL, MySQL, MariaDB, SQL Server and SQLite, +//! foreign keys, on PostgreSQL, MySQL, MariaDB, SQL Server, SQLite and DuckDB, //! through the app's own connector and session, as the tab reads it. //! //! `node_00` points nowhere. Every other `node_N` points at its parent @@ -51,6 +51,7 @@ enum Engine { Mariadb, Mssql, Sqlite, + Duckdb, } impl Engine { @@ -58,7 +59,7 @@ impl Engine { match self { Self::Mysql | Self::Mariadb => format!("`{name}`"), Self::Mssql => format!("[{name}]"), - Self::Postgres | Self::Sqlite => format!("\"{name}\""), + Self::Postgres | Self::Sqlite | Self::Duckdb => format!("\"{name}\""), } } } @@ -106,7 +107,7 @@ struct Server { request: ConnectRequest, /// The schema the diagram reads. scope: Option, - /// The container, or the SQLite file's folder, which goes when this + /// The container, or the file's folder, which goes when this /// does. _fixture: Box, } @@ -189,7 +190,7 @@ impl Server { Engine::Postgres => Self::postgres().await, Engine::Mysql | Engine::Mariadb => Self::mysql(engine).await, Engine::Mssql => Self::mssql().await, - Engine::Sqlite => Self::sqlite().await, + Engine::Sqlite | Engine::Duckdb => Self::file(engine).await, } } @@ -268,11 +269,21 @@ impl Server { } /// The main schema of a new file, which the sidebar lists with no - /// schema name. - async fn sqlite() -> Self { + /// schema name for SQLite and as `main` for DuckDB. + async fn file(engine: Engine) -> Self { let dir = tempfile::tempdir().expect("a temporary directory"); - let path: PathBuf = dir.path().join("diagram.sqlite3"); - let driver: Arc = Arc::new(tablepro_driver_sqlite::SqliteDriver); + let (driver, path, scope): (Arc, PathBuf, _) = match engine { + Engine::Duckdb => ( + Arc::new(tablepro_driver_duckdb::DuckDbDriver), + dir.path().join("diagram.duckdb"), + Some("main".to_owned()), + ), + _ => ( + Arc::new(tablepro_driver_sqlite::SqliteDriver), + dir.path().join("diagram.sqlite3"), + None, + ), + }; let options = ConnectOptions { target: ConnectTarget::File(FileEndpoint::create_new(path.clone()).expect("an absolute path")), workspace_settings: WorkspaceSettings::workspace(false), @@ -283,7 +294,7 @@ impl Server { .connect(options, CancellationToken::new()) .await .expect("the new file"); - apply(admin.as_ref(), &create_statements(Engine::Sqlite, None)).await; + apply(admin.as_ref(), &create_statements(engine, None)).await; admin.close().await.expect("the file closes"); Self { request: ConnectRequest { @@ -293,7 +304,7 @@ impl Server { workspace_settings: WorkspaceSettings::workspace(false), safe_mode: tablepro_core::SafeModeLevel::Off, }, - scope: None, + scope, _fixture: Box::new(dir), } } @@ -454,3 +465,8 @@ async fn sql_server_schema_of_50_tables_draws_its_keys() { async fn sqlite_file_of_50_tables_draws_its_keys() { case(Engine::Sqlite).await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn duckdb_file_of_50_tables_draws_its_keys() { + case(Engine::Duckdb).await; +} diff --git a/linux/crates/app/src/ui/error_text/driver.rs b/linux/crates/app/src/ui/error_text/driver.rs index d80b03b7e5..e51e6c6abf 100644 --- a/linux/crates/app/src/ui/error_text/driver.rs +++ b/linux/crates/app/src/ui/error_text/driver.rs @@ -182,6 +182,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)]), ServerCode::Cql { .. } => crate::i18n::gettext_f("CQL error {code}", &[("code", &number)]), // Redis names its errors by a word, such as WRONGTYPE. ServerCode::Redis { .. } => number, diff --git a/linux/crates/app/src/ui/error_text/unsupported.rs b/linux/crates/app/src/ui/error_text/unsupported.rs index 7679416338..2610de428a 100644 --- a/linux/crates/app/src/ui/error_text/unsupported.rs +++ b/linux/crates/app/src/ui/error_text/unsupported.rs @@ -40,6 +40,9 @@ pub(super) fn unsupported_message(feature: UnsupportedFeature) -> String { "This database has no dates outside the years {first} to {last}.", &[("first", &first.to_string()), ("last", &last.to_string())], ), + UnsupportedFeature::HiddenAttach => { + gettext("The editor cannot attach a HIDDEN database, which no other window could see is open.") + } UnsupportedFeature::ServerPaging => gettext("This database does not page tables on the server."), UnsupportedFeature::Tables => gettext("This database holds keys, not tables."), UnsupportedFeature::Keys => gettext("This database holds tables, not keys."), @@ -75,6 +78,7 @@ mod tests { UnsupportedFeature::NumberWiderThan { digits: 38 }, UnsupportedFeature::DecimalOutOfRange { digits: 38 }, UnsupportedFeature::DateOutsideYears { first: 1, last: 9999 }, + UnsupportedFeature::HiddenAttach, UnsupportedFeature::ServerPaging, UnsupportedFeature::Tables, UnsupportedFeature::Keys, diff --git a/linux/crates/app/src/ui/picker/connection_row.rs b/linux/crates/app/src/ui/picker/connection_row.rs index b1b16f154a..e87e589b86 100644 --- a/linux/crates/app/src/ui/picker/connection_row.rs +++ b/linux/crates/app/src/ui/picker/connection_row.rs @@ -14,7 +14,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; use crate::ui::connection_appearance::{color_dot, icon_image, tag_pills, tags_description}; @@ -29,16 +29,16 @@ pub(crate) struct RowChoices { /// menu holds the rest. `tasks` reads the file a portal path stands /// for; `None` names the file alone. pub(crate) fn connection_row(object: &glib::Object, choices: &RowChoices, 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) @@ -49,7 +49,7 @@ pub(crate) fn connection_row(object: &glib::Object, choices: &RowChoices, tasks: row.add_prefix(&icon_image(saved.icon)); let carried: Vec = carried_tags(&saved, &choices.tags).into_iter().cloned().collect(); row.add_suffix(&tag_pills(&carried)); - describe_row(&row, &subtitle_for(&saved, None), &carried); + describe_row(&row, &subtitle_for(&saved, file_based, None), &carried); // One menu rather than a button per action: the row gains actions as // the app does, and a row of icons stops reading as a row. let menu_button = gtk::MenuButton::builder() @@ -68,7 +68,7 @@ pub(crate) fn connection_row(object: &glib::Object, choices: &RowChoices, tasks: 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, carried, tasks); + show_host_path(&row, &saved, file_based, carried, tasks); } row.upcast() } @@ -165,16 +165,22 @@ fn secondary_click(menu_button: >k::MenuButton) -> gtk::GestureClick { /// 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, tags: Vec, tasks: &Tasks) { +fn show_host_path( + row: &adw::ActionRow, + saved: &SavedConnection, + file_based: bool, + tags: Vec, + 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()) { - let subtitle = subtitle_for(&saved, Some(&host)); + let subtitle = subtitle_for(&saved, true, Some(&host)); row.set_subtitle(&subtitle); describe_row(&row, &subtitle, &tags); } @@ -216,7 +222,7 @@ mod tests { } fn row_for(saved: SavedConnection, tags: Vec) -> adw::ActionRow { - let object = SavedConnectionObject::new(saved); + let object = SavedConnectionObject::new(saved, false); let choices = RowChoices { groups: Vec::new(), tags: tags.into(), diff --git a/linux/crates/app/src/ui/picker/tests.rs b/linux/crates/app/src/ui/picker/tests.rs index 88fd694655..ca1ec32f35 100644 --- a/linux/crates/app/src/ui/picker/tests.rs +++ b/linux/crates/app/src/ui/picker/tests.rs @@ -57,7 +57,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 83daa2b1c9..d19800dbbf 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; @@ -780,7 +780,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, @@ -1159,9 +1159,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 586bb917ee..35a3c26bad 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, ConnectionTag, ConnectionTagName, 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 9c7d0a4525..65a1151126 100644 --- a/linux/crates/app/src/ui/session_window/tests/mod.rs +++ b/linux/crates/app/src/ui/session_window/tests/mod.rs @@ -134,6 +134,13 @@ impl TestWindow { (false, false) => &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 c1e85b566a..023270f304 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; @@ -80,6 +81,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 93a42414c7..206eec37f7 100644 --- a/linux/crates/app/src/ui/structure_tab/index_dialog.rs +++ b/linux/crates/app/src/ui/structure_tab/index_dialog.rs @@ -36,6 +36,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. @@ -45,7 +47,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")); @@ -84,6 +91,7 @@ impl IndexDialog { shown_keys: RefCell::default(), columns, existing, + descending, draft: RefCell::default(), name_edited: Cell::new(false), refresh_queued: Cell::new(false), @@ -209,15 +217,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() @@ -230,14 +240,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), @@ -297,10 +309,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), ) } @@ -425,6 +442,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/app/src/ui/tags/manage_tags_dialog.rs b/linux/crates/app/src/ui/tags/manage_tags_dialog.rs index 5fc72f1e92..fe0ed4676a 100644 --- a/linux/crates/app/src/ui/tags/manage_tags_dialog.rs +++ b/linux/crates/app/src/ui/tags/manage_tags_dialog.rs @@ -286,7 +286,10 @@ mod tests { fn list() -> (Rc, tempfile::TempDir) { let dir = tempfile::tempdir().expect("a temporary directory"); let store = ConnectionStore::new(&StoragePaths::under(dir.path(), "tablepro", crate::config::APP_ID)); - (SavedConnectionList::follow(&store), dir) + ( + SavedConnectionList::follow(&store, std::sync::Arc::new(crate::build_registry())), + dir, + ) } fn ready(connections: Vec, tags: Vec) -> ConnectionListState { diff --git a/linux/crates/core/src/browse/build_export.rs b/linux/crates/core/src/browse/build_export.rs index 3639416d96..a84e6b2b9c 100644 --- a/linux/crates/core/src/browse/build_export.rs +++ b/linux/crates/core/src/browse/build_export.rs @@ -13,18 +13,19 @@ 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 sorts_the_table = dialect.key_order_sorts_the_table() + || matches!(request.identity, RowIdentity::EngineRowId(id) if id.is_heap_address()); // An engine that pages on the server reads in its own order, which is // already the same for two exports of an unchanged table. - let order = match (dialect.table_paging(), heap_order_only) { + let order = match (dialect.table_paging(), user.is_empty() && sorts_the_table) { (TablePaging::ServerState, _) if !user.is_empty() => return Err(BrowseSqlError::SortUnsupported), (TablePaging::ServerState, _) | (TablePaging::InStatement, true) => Vec::new(), (TablePaging::InStatement, 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 dff5002242..29f5f7e025 100644 --- a/linux/crates/core/src/browse/tests.rs +++ b/linux/crates/core/src/browse/tests.rs @@ -525,9 +525,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, @@ -560,6 +564,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 da489f9960..90d095f1d6 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. @@ -107,12 +108,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 { @@ -127,6 +136,7 @@ impl DriverCapabilities { #[cfg(test)] mod tests { use super::*; + use crate::file_format::FileSignature; const NETWORK: DriverCapabilities = DriverCapabilities { data_model: DataModel::Relational, @@ -147,14 +157,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"); } @@ -162,6 +182,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 598f33eddf..741188a38d 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 288c232998..03fcfc8c35 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, @@ -53,6 +54,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/ddl_sql.rs b/linux/crates/core/src/ddl/ddl_sql.rs index 2d9b4acbbd..357643b37a 100644 --- a/linux/crates/core/src/ddl/ddl_sql.rs +++ b/linux/crates/core/src/ddl/ddl_sql.rs @@ -117,7 +117,7 @@ pub fn foreign_key_sql(dialect: &dyn SqlDialect, table: &TableRef, key: &Foreign "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})", dialect.quote_identifier(&key.name), column_list(dialect, &key.columns), - dialect.qualify(&referenced), + dialect.portable_name(&referenced), column_list(dialect, &key.ref_columns) ); for (clause, action) in [("ON DELETE", key.on_delete), ("ON UPDATE", key.on_update)] { diff --git a/linux/crates/core/src/ddl/table_changes.rs b/linux/crates/core/src/ddl/table_changes.rs index 6a5b5f6a33..6781dcff1d 100644 --- a/linux/crates/core/src/ddl/table_changes.rs +++ b/linux/crates/core/src/ddl/table_changes.rs @@ -93,6 +93,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)?; @@ -204,6 +209,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 08a37131c9..ecb6481a0e 100644 --- a/linux/crates/core/src/ddl/tests.rs +++ b/linux/crates/core/src/ddl/tests.rs @@ -424,6 +424,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 f11ccc1fc4..205bcc8bb6 100644 --- a/linux/crates/core/src/dialect/sql_dialect.rs +++ b/linux/crates/core/src/dialect/sql_dialect.rs @@ -125,6 +125,14 @@ pub trait SqlDialect: Send + Sync + std::fmt::Debug { } } + /// The table's name as text that stands outside this connection: in + /// an export replayed elsewhere, and in a foreign key's REFERENCES. + /// DuckDB names its file's catalogue in `qualify` and leaves it out + /// here; the other engines name the table the same in both. + fn portable_name(&self, table: &TableRef) -> String { + self.qualify(table) + } + /// How a column is selected. A type the driver cannot decode is /// asked for as text instead, which is what `read_form` says. fn projection(&self, column: &ColumnInfo) -> String { @@ -143,6 +151,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 85bcf71d19..aa0728058e 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, insert_conflict: InsertConflict, @@ -66,6 +67,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, insert_conflict: InsertConflict::Refused, @@ -105,6 +107,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 } } @@ -258,6 +267,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/export/sql.rs b/linux/crates/core/src/export/sql.rs index 58ffd21b96..5f573017f0 100644 --- a/linux/crates/core/src/export/sql.rs +++ b/linux/crates/core/src/export/sql.rs @@ -62,7 +62,7 @@ pub(super) fn insert_shape( if written.is_empty() { return None; } - let table_sql = dialect.qualify(table); + let table_sql = dialect.portable_name(table); Some(InsertShape { start: format!("INSERT INTO {table_sql} ({}) VALUES ", written.join(", ")), table_sql, @@ -129,7 +129,7 @@ pub fn render_insert( columns: &[ColumnInfo], cells: &[CellInput], ) -> Result { - let table_sql = dialect.qualify(table); + let table_sql = dialect.portable_name(table); let mut names = Vec::with_capacity(columns.len()); let mut values = Vec::with_capacity(columns.len()); for (index, column) in columns.iter().enumerate() { 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/file_opens.rs b/linux/crates/core/src/file_opens.rs new file mode 100644 index 0000000000..02cbb31f75 --- /dev/null +++ b/linux/crates/core/src/file_opens.rs @@ -0,0 +1,226 @@ +//! How SQLite and DuckDB share one process's database files. +//! +//! Both lock a file with POSIX record locks, which belong to the process: +//! closing any descriptor the process has on the file drops them all, the +//! other engine's included. So neither engine opens a file the other has +//! open, which it would close again on finding no database of its own in +//! it (`claim_file`, `claimed_by_another`), and a header read, which opens +//! and closes a descriptor, never runs while an engine opens a file and +//! has no descriptor on it yet for the reader to see (`opening_file`). + +use std::collections::{HashMap, HashSet}; +use std::os::unix::fs::MetadataExt; +use std::path::Path; +use std::sync::PoisonError; + +use crate::busy_reason::BusyReason; +use crate::error::DriverError; + +static FILE_OPENS: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +/// The engine holding each claimed file, and how many claims it holds. +static CLAIMS: std::sync::Mutex>> = std::sync::Mutex::new(None); + +/// Held by an engine from before it opens a database file until it holds +/// a descriptor on it, and by a header read throughout. +pub async fn opening_file() -> tokio::sync::MutexGuard<'static, ()> { + FILE_OPENS.lock().await +} + +/// `opening_file` for a thread that may block, never an async task. +pub fn opening_file_blocking() -> tokio::sync::MutexGuard<'static, ()> { + FILE_OPENS.blocking_lock() +} + +/// A file whatever path names it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct FileKey { + device: u64, + inode: u64, +} + +impl FileKey { + fn of(path: &Path) -> Option { + std::fs::metadata(path).ok().map(|metadata| Self { + device: metadata.dev(), + inode: metadata.ino(), + }) + } +} + +/// One engine's hold on a file, given up when dropped. +#[derive(Debug)] +#[must_use = "the claim ends when it is dropped"] +pub struct FileClaim { + key: FileKey, +} + +impl Drop for FileClaim { + fn drop(&mut self) { + let mut claims = CLAIMS.lock().unwrap_or_else(PoisonError::into_inner); + let Some(claims) = claims.as_mut() else { + return; + }; + if let Some((_, count)) = claims.get_mut(&self.key) { + *count = count.saturating_sub(1); + if *count == 0 { + claims.remove(&self.key); + } + } + } +} + +/// Record that `engine` has the file at `path` open, or refuse while +/// another engine has it. `None` when nothing is at `path`. +pub fn claim_file(path: &Path, engine: &'static str) -> Result, DriverError> { + let Some(key) = FileKey::of(path) else { + return Ok(None); + }; + let mut claims = CLAIMS.lock().unwrap_or_else(PoisonError::into_inner); + let claims = claims.get_or_insert_with(HashMap::new); + match claims.get_mut(&key) { + Some((holder, _)) if *holder != engine => Err(DriverError::Busy(BusyReason::FileLocked)), + Some((_, count)) => { + *count += 1; + Ok(Some(FileClaim { key })) + } + None => { + claims.insert(key, (engine, 1)); + Ok(Some(FileClaim { key })) + } + } +} + +/// Whether an engine other than `engine` has the file at `path` open. A +/// claim outlives a DETACH, which closes the file, so only one whose file +/// a descriptor of this process still names counts. +pub fn claimed_by_another(path: &Path, engine: &str) -> bool { + let Some(key) = FileKey::of(path) else { + return false; + }; + let claimed = { + let claims = CLAIMS.lock().unwrap_or_else(PoisonError::into_inner); + claims + .as_ref() + .and_then(|claims| claims.get(&key)) + .is_some_and(|(holder, _)| *holder != engine) + }; + claimed && held_keys().contains(&key) +} + +/// Whether an engine other than `engine` has any file open, for a check +/// that cannot tell which file an open will reach. +pub fn any_claimed_by_another(engine: &str) -> bool { + let keys: Vec = { + let claims = CLAIMS.lock().unwrap_or_else(PoisonError::into_inner); + claims + .as_ref() + .map(|claims| { + claims + .iter() + .filter(|(_, (holder, _))| *holder != engine) + .map(|(key, _)| *key) + .collect() + }) + .unwrap_or_default() + }; + if keys.is_empty() { + return false; + } + let held = held_keys(); + keys.iter().any(|key| held.contains(key)) +} + +/// Whether a descriptor this process has open names the file at `path`. +pub fn held_by_this_process(path: &Path) -> bool { + FileKey::of(path).is_some_and(|key| held_keys().contains(&key)) +} + +/// The files the descriptors of this process name. +fn held_keys() -> HashSet { + let Ok(descriptors) = std::fs::read_dir("/proc/self/fd") else { + return HashSet::new(); + }; + descriptors + .flatten() + .filter_map(|descriptor| FileKey::of(&descriptor.path())) + .collect() +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + /// A directory of the test's own, removed when it ends. + struct Scratch(PathBuf); + + impl Scratch { + fn new() -> Self { + let path = std::env::temp_dir().join(format!("tablepro-file-opens-{}", uuid::Uuid::new_v4().simple())); + std::fs::create_dir(&path).unwrap(); + Self(path) + } + + fn path(&self) -> &Path { + &self.0 + } + } + + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn a_file_one_engine_holds_is_refused_to_the_other_until_let_go() { + let directory = Scratch::new(); + let path = directory.path().join("held.db"); + std::fs::write(&path, b"x").unwrap(); + let link = directory.path().join("link.db"); + std::fs::hard_link(&path, &link).unwrap(); + + let first = claim_file(&path, "duckdb").unwrap(); + let second = claim_file(&link, "duckdb").unwrap(); + let open = std::fs::File::open(&path).unwrap(); + + assert!(claimed_by_another(&link, "sqlite")); + assert!(!claimed_by_another(&path, "duckdb")); + assert!(any_claimed_by_another("sqlite")); + assert_eq!( + claim_file(&path, "sqlite").unwrap_err(), + DriverError::Busy(BusyReason::FileLocked) + ); + drop(first); + assert!(claimed_by_another(&path, "sqlite"), "one claim is still held"); + drop(open); + assert!( + !claimed_by_another(&path, "sqlite"), + "the engine closed the file, as a DETACH does, so the claim is idle" + ); + drop(second); + assert!(!claimed_by_another(&path, "sqlite")); + assert!( + claim_file(&directory.path().join("missing.db"), "sqlite") + .unwrap() + .is_none() + ); + } + + #[test] + fn a_descriptor_on_any_name_of_a_file_holds_it() { + let directory = Scratch::new(); + let path = directory.path().join("open.db"); + std::fs::write(&path, b"x").unwrap(); + let link = directory.path().join("other-name.db"); + std::fs::hard_link(&path, &link).unwrap(); + + let open = std::fs::File::open(&path).unwrap(); + + assert!(held_by_this_process(&link)); + drop(open); + assert!(!held_by_this_process(&link)); + } +} diff --git a/linux/crates/core/src/lib.rs b/linux/crates/core/src/lib.rs index 02c7839541..ae13c95a55 100644 --- a/linux/crates/core/src/lib.rs +++ b/linux/crates/core/src/lib.rs @@ -25,7 +25,9 @@ mod error_category; pub mod export; mod external_access; mod file_endpoint; +mod file_format; mod file_open_mode; +mod file_opens; pub mod filter; mod foreign_key_check; pub mod foreign_key_info; @@ -107,7 +109,12 @@ pub use error::DriverError; pub use error_category::ErrorCategory; pub use external_access::{ExternalAccess, ExternalGate, ExternalRefusal}; pub use file_endpoint::FileEndpoint; +pub use file_format::{FileFormat, FileSignature}; pub use file_open_mode::FileOpenMode; +pub use file_opens::{ + FileClaim, any_claimed_by_another, claim_file, claimed_by_another, held_by_this_process, opening_file, + opening_file_blocking, +}; pub use filter::{ BuildFilterError, Combinator, FilterOp, FilterRule, FilterSet, FilterValue, build_filter, operators_for, validate_raw_predicate, diff --git a/linux/crates/core/src/registry.rs b/linux/crates/core/src/registry.rs index 81159b8af2..8b66aa9c87 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,214 @@ 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, DataModel, DriverCapabilities, EndpointKind, EstimateQuality, ReadOnlyEnforcement, + ScriptExecution, StatementStop, + }; + 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 { + data_model: DataModel::Relational, + endpoint, + auth_modes: &[], + username_required: false, + 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, + statement_stop: StatementStop::Server, + } + } + + 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 1c725325f4..3b9cb93fe3 100644 --- a/linux/crates/core/src/server_diagnostics.rs +++ b/linux/crates/core/src/server_diagnostics.rs @@ -30,6 +30,11 @@ pub enum ServerCode { Cql { code: i32, }, + /// DuckDB names its errors by kind rather than number, as the + /// `Constraint` of "Constraint Error: ...". + DuckDb { + error_type: String, + }, /// The word a Redis error reply opens with, such as `WRONGTYPE`. Redis { prefix: String, @@ -55,6 +60,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), Self::Cql { code } => write!(f, "{code:#06x}"), Self::Redis { prefix } => f.write_str(prefix), } diff --git a/linux/crates/core/src/sql_syntax/builtin_snippet.rs b/linux/crates/core/src/sql_syntax/builtin_snippet.rs index ac621cee28..aa56f73f5f 100644 --- a/linux/crates/core/src/sql_syntax/builtin_snippet.rs +++ b/linux/crates/core/src/sql_syntax/builtin_snippet.rs @@ -133,6 +133,22 @@ const CLICKHOUSE: [BuiltinSnippet; 7] = [ ), ]; +// DuckDB numbers a key only from a sequence, which is a statement of its +// own, so the key is a UUID it makes. +const DUCKDB: [BuiltinSnippet; 7] = [ + SELECT, + INSERT, + UPDATE, + DELETE, + JOIN, + BuiltinSnippet::new( + "ctab", + "CREATE TABLE", + "CREATE TABLE ${1:table_name} (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n ${2:name} ${3:VARCHAR} NOT NULL$0\n);", + ), + CREATE_INDEX, +]; + // CQL has no join, and nothing numbers a key, so a row's key is its own // uuid. const CQL: [BuiltinSnippet; 6] = [ @@ -157,6 +173,7 @@ pub fn builtin_snippets(grammar: SqlGrammar) -> &'static [BuiltinSnippet] { SqlGrammar::Sqlite => &SQLITE, SqlGrammar::MsSql => &MSSQL, SqlGrammar::ClickHouse => &CLICKHOUSE, + SqlGrammar::DuckDb => &DUCKDB, SqlGrammar::Cql => &CQL, SqlGrammar::Redis => &[], } diff --git a/linux/crates/core/src/sql_syntax/grammar.rs b/linux/crates/core/src/sql_syntax/grammar.rs index 20f307729c..fa551fa95d 100644 --- a/linux/crates/core/src/sql_syntax/grammar.rs +++ b/linux/crates/core/src/sql_syntax/grammar.rs @@ -8,6 +8,7 @@ pub enum SqlGrammar { Sqlite, MsSql, ClickHouse, + DuckDb, /// Cassandra and ScyllaDB. Cql, Redis, @@ -15,12 +16,13 @@ pub enum SqlGrammar { impl SqlGrammar { /// The grammars that are SQL. - pub const SQL: [SqlGrammar; 6] = [ + pub const SQL: [SqlGrammar; 7] = [ SqlGrammar::PostgreSql, SqlGrammar::MySql, SqlGrammar::Sqlite, SqlGrammar::MsSql, SqlGrammar::ClickHouse, + SqlGrammar::DuckDb, SqlGrammar::Cql, ]; } @@ -32,7 +34,8 @@ impl SqlGrammar { /// `None` for a language that is not SQL. pub(crate) fn parser_dialect(self) -> Option<&'static dyn sqlparser::dialect::Dialect> { use sqlparser::dialect::{ - ClickHouseDialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, + ClickHouseDialect, DuckDbDialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, + SQLiteDialect, }; static POSTGRES: PostgreSqlDialect = PostgreSqlDialect {}; @@ -40,6 +43,7 @@ impl SqlGrammar { static SQLITE: SQLiteDialect = SQLiteDialect {}; static MSSQL: MsSqlDialect = MsSqlDialect {}; static CLICKHOUSE: ClickHouseDialect = ClickHouseDialect {}; + static DUCKDB: DuckDbDialect = DuckDbDialect {}; // sqlparser has no CQL dialect. CQL's expressions are a subset of // what the generic one reads, and its types are parsed by the // driver's own dialect. @@ -51,6 +55,7 @@ impl SqlGrammar { Self::Sqlite => Some(&SQLITE), Self::MsSql => Some(&MSSQL), Self::ClickHouse => Some(&CLICKHOUSE), + Self::DuckDb => Some(&DUCKDB), Self::Cql => Some(&GENERIC), Self::Redis => None, } diff --git a/linux/crates/core/src/sql_syntax/history_recording.rs b/linux/crates/core/src/sql_syntax/history_recording.rs index aed06aa5b3..1f7c3171aa 100644 --- a/linux/crates/core/src/sql_syntax/history_recording.rs +++ b/linux/crates/core/src/sql_syntax/history_recording.rs @@ -448,7 +448,7 @@ mod tests { proptest! { #[test] - fn any_text_classifies_without_panicking(sql in "\\PC{0,120}", grammar in 0usize..5) { + fn any_text_classifies_without_panicking(sql in "\\PC{0,120}", grammar in 0usize..SqlGrammar::SQL.len()) { let grammar = SqlGrammar::SQL[grammar]; let _ = classify(grammar, &sql); } diff --git a/linux/crates/core/src/sql_syntax/keywords.rs b/linux/crates/core/src/sql_syntax/keywords.rs index ca89c14ddc..3f22fab30f 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", +]; + const CQL: &[&str] = &[ "ALLOW", "APPLY", @@ -178,6 +205,7 @@ impl SqlGrammar { Self::Sqlite => (COMMON, SQLITE), Self::MsSql => (COMMON, MSSQL), Self::ClickHouse => (COMMON, CLICKHOUSE), + Self::DuckDb => (COMMON, DUCKDB), Self::Cql => (COMMON, CQL), Self::Redis => (&[], &[]), }; @@ -202,6 +230,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 c04a09ea28..2f98f52a0a 100644 --- a/linux/crates/core/src/sql_syntax/reserved_words.rs +++ b/linux/crates/core/src/sql_syntax/reserved_words.rs @@ -4,8 +4,9 @@ //! 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. CQL's is `reserved_keywords.txt` from the -//! Apache Cassandra 5.0 jar. +//! wherever they stand. DuckDB's are the `reserved` and `type_function` +//! words of its `duckdb_keywords()`. CQL's is `reserved_keywords.txt` from +//! the Apache Cassandra 5.0 jar. use crate::sql_syntax::SqlGrammar; @@ -85,6 +86,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 +"; + const CQL: &str = " ADD ALLOW ALTER AND APPLY ASC AUTHORIZE BATCH BEGIN BY COLUMNFAMILY CREATE DELETE DESC DESCRIBE DROP ENTRIES EXECUTE FROM FULL GRANT IF IN INDEX INFINITY INSERT INTO IS KEYSPACE LIMIT MATERIALIZED MODIFY @@ -137,6 +149,7 @@ impl SqlGrammar { Self::Sqlite => SQLITE, Self::MsSql => MSSQL, Self::ClickHouse => CLICKHOUSE, + Self::DuckDb => DUCKDB, Self::Cql => CQL, Self::Redis => "", } @@ -174,6 +187,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] @@ -224,5 +241,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 59c9dfbdfd..bdf70b090e 100644 --- a/linux/crates/core/src/sql_syntax/script/lexical_settings.rs +++ b/linux/crates/core/src/sql_syntax/script/lexical_settings.rs @@ -9,9 +9,12 @@ 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 | SqlGrammar::Cql | SqlGrammar::Redis => { - false - } + SqlGrammar::PostgreSql + | SqlGrammar::Sqlite + | SqlGrammar::MsSql + | SqlGrammar::DuckDb + | SqlGrammar::Cql + | SqlGrammar::Redis => 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 64eb406cde..2504db5fbc 100644 --- a/linux/crates/core/src/sql_syntax/script/script_rules.rs +++ b/linux/crates/core/src/sql_syntax/script/script_rules.rs @@ -243,6 +243,33 @@ const CQL: 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, + digit_separators: true, + word_start_extra: b"", + word_part_extra: b"$", + }, +}; + /// `None` for Redis, whose commands have no SQL rules. pub(crate) fn rules_for(grammar: SqlGrammar) -> Option<&'static ScriptRules> { match grammar { @@ -251,6 +278,7 @@ pub(crate) fn rules_for(grammar: SqlGrammar) -> Option<&'static ScriptRules> { SqlGrammar::Sqlite => Some(&SQLITE), SqlGrammar::MsSql => Some(&MSSQL), SqlGrammar::ClickHouse => Some(&CLICKHOUSE), + SqlGrammar::DuckDb => Some(&DUCKDB), SqlGrammar::Cql => Some(&CQL), SqlGrammar::Redis => None, } diff --git a/linux/crates/core/src/unsupported_feature.rs b/linux/crates/core/src/unsupported_feature.rs index 94f3b42390..18a8d161f0 100644 --- a/linux/crates/core/src/unsupported_feature.rs +++ b/linux/crates/core/src/unsupported_feature.rs @@ -37,6 +37,8 @@ pub enum UnsupportedFeature { first: i32, last: i32, }, + /// A DuckDB `ATTACH ... (HIDDEN)`, a database DuckDB lists nowhere. + HiddenAttach, /// Pages the server keeps itself, by a paging state. ServerPaging, /// Tables, columns and SQL, on an engine that has keys instead. @@ -69,6 +71,7 @@ impl fmt::Display for UnsupportedFeature { Self::NumberWiderThan { digits } => write!(f, "a number wider than {digits} digits"), Self::DecimalOutOfRange { digits } => write!(f, "a decimal NaN, infinity or wider than {digits} digits"), Self::DateOutsideYears { first, last } => write!(f, "a date outside years {first} to {last}"), + Self::HiddenAttach => f.write_str("a HIDDEN attached database"), Self::ServerPaging => f.write_str("server-side paging"), Self::Tables => f.write_str("tables"), Self::Keys => f.write_str("keys"), diff --git a/linux/crates/core/tests/script_corpus.rs b/linux/crates/core/tests/script_corpus.rs index 45a11a7af7..723a65ba6a 100644 --- a/linux/crates/core/tests/script_corpus.rs +++ b/linux/crates/core/tests/script_corpus.rs @@ -2,7 +2,8 @@ use std::num::NonZeroU32; use proptest::prelude::*; use sqlparser::dialect::{ - ClickHouseDialect, Dialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, + ClickHouseDialect, Dialect, DuckDbDialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, + SQLiteDialect, }; use sqlparser::tokenizer::{Token, Tokenizer, TokenizerError}; use tablepro_core::sql_syntax::SqlGrammar; @@ -476,6 +477,30 @@ fn cql_unterminated_double_dollar_keeps_prior_statements() { ); } +#[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;"; @@ -639,6 +664,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;"), (SqlGrammar::Cql, "SELECT 'x''y;', \"a;b\" FROM t; -- c;\nSELECT 2;"), ]; @@ -665,6 +691,7 @@ fn sqlparser_semicolons(grammar: SqlGrammar, text: &str) -> Result, T SqlGrammar::Sqlite => &SQLiteDialect {}, SqlGrammar::MsSql => &MsSqlDialect {}, SqlGrammar::ClickHouse => &ClickHouseDialect {}, + SqlGrammar::DuckDb => &DuckDbDialect {}, SqlGrammar::Cql => &GenericDialect {}, // Commands have no SQL tokenizer to agree with, and no agreed case // is a Redis one. @@ -737,6 +764,9 @@ fn a_digit_separator_keeps_a_number_whole_where_the_engine_reads_one() { ["1_0e", "2"], "a name, then a number" ); + // DuckDB 1.5 reads both as one number and refuses `1._5`. + assert_eq!(numbers(SqlGrammar::DuckDb, "1_0e+2"), ["1_0e+2"]); + assert_eq!(numbers(SqlGrammar::DuckDb, "1_000.000_5"), ["1_000.000_5"]); } #[test] diff --git a/linux/crates/drivers/cassandra/src/ddl.rs b/linux/crates/drivers/cassandra/src/ddl.rs index 07a197310d..2f6caec698 100644 --- a/linux/crates/drivers/cassandra/src/ddl.rs +++ b/linux/crates/drivers/cassandra/src/ddl.rs @@ -24,6 +24,7 @@ static CAPABILITIES: DdlCapabilities = DdlCapabilities { create_index: false, drop_index: true, expression_index_keys: false, + descending_index_keys: false, partial_indexes: false, add_foreign_key: false, drop_foreign_key: false, diff --git a/linux/crates/drivers/clickhouse/src/ddl.rs b/linux/crates/drivers/clickhouse/src/ddl.rs index 88d10b3dc8..6579303080 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..3dc382ad72 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/capabilities.rs @@ -0,0 +1,38 @@ +use tablepro_core::meta::ObjectKind; +use tablepro_core::{ + CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, FileFormat, FileSignature, + ReadOnlyEnforcement, ScriptExecution, StatementStop, 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 { + data_model: tablepro_core::DataModel::Relational, + endpoint: EndpointKind::File(FILE_FORMAT), + auth_modes: &[], + username_required: false, + 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, + statement_stop: StatementStop::Server, +}; diff --git a/linux/crates/drivers/duckdb/src/catalog/declared_columns.rs b/linux/crates/drivers/duckdb/src/catalog/declared_columns.rs new file mode 100644 index 0000000000..654fb128dd --- /dev/null +++ b/linux/crates/drivers/duckdb/src/catalog/declared_columns.rs @@ -0,0 +1,160 @@ +//! What a table's CREATE TABLE text says about its columns. +//! +//! DuckDB's catalogue functions list a generated column's expression as +//! its default and a column's collation nowhere, so the only record of +//! either is the CREATE TABLE text DuckDB keeps, where a column reads +//! `name type [COLLATE collation] [GENERATED ALWAYS AS (expression)]`. + +use tablepro_core::sql_syntax::SqlGrammar; +use tablepro_core::sql_syntax::script::{LexicalSettings, ScriptPlan, ScriptTokenKind}; + +/// One element of the column list: a column, or a table constraint, +/// which no column name matches. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DeclaredColumn { + pub(crate) name: String, + pub(crate) generated: bool, + /// The collation it compares under instead of byte for byte, as the + /// text spells it, `nocase.noaccent` for two. + pub(crate) collation: Option, +} + +/// The column list of `create_sql`, with names as the table's own text +/// spells them. +pub(crate) fn declared_columns(create_sql: &str) -> Vec { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(create_sql, grammar, LexicalSettings::default_for(grammar)); + let mut declared = Vec::new(); + let mut depth = 0usize; + // The element's tokens and the depth each sits at, 1 being the + // column list itself. + let mut element: Vec<(ScriptTokenKind, &str, usize)> = Vec::new(); + for token in plan.tokens(0..create_sql.len()).filter(|token| !token.kind.is_trivia()) { + 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 { + declared.extend(take(&mut element)); + continue; + } + } + ScriptTokenKind::Punct if text == "," && depth == 1 => { + declared.extend(take(&mut element)); + continue; + } + _ => {} + } + if depth >= 1 { + element.push((token.kind, text, depth)); + } + } + declared +} + +/// Close one element of the column list. +fn take(element: &mut Vec<(ScriptTokenKind, &str, usize)>) -> Option { + let own: Vec<(ScriptTokenKind, &str)> = element + .iter() + .filter(|(_, _, depth)| *depth == 1) + .map(|(kind, text, _)| (*kind, *text)) + .collect(); + let words: Vec = own + .iter() + .filter(|(kind, _)| *kind == ScriptTokenKind::Word) + .map(|(_, text)| text.to_ascii_uppercase()) + .collect(); + let name = match element.first() { + Some((ScriptTokenKind::QuotedIdentifier, name, _)) => Some(unquote(name)), + Some((ScriptTokenKind::Word, name, _)) => Some((*name).to_owned()), + _ => None, + }; + element.clear(); + Some(DeclaredColumn { + name: name?, + generated: words.windows(3).any(|window| window == ["GENERATED", "ALWAYS", "AS"]), + collation: collation(&own), + }) +} + +/// The name after COLLATE: one or more names joined by dots. +fn collation(tokens: &[(ScriptTokenKind, &str)]) -> Option { + let is_name = |kind: ScriptTokenKind| matches!(kind, ScriptTokenKind::Word | ScriptTokenKind::QuotedIdentifier); + let at = tokens + .iter() + .position(|(kind, text)| *kind == ScriptTokenKind::Word && text.eq_ignore_ascii_case("COLLATE"))?; + let mut rest = tokens.get(at + 1..)?.iter(); + let mut name = rest.next().filter(|(kind, _)| is_name(*kind))?.1.to_owned(); + while let (Some((ScriptTokenKind::Punct, ".")), Some((kind, part))) = (rest.next().copied(), rest.next().copied()) { + if !is_name(kind) { + break; + } + name.push('.'); + name.push_str(part); + } + Some(name) +} + +fn unquote(quoted: &str) -> String { + quoted + .strip_prefix('"') + .and_then(|inner| inner.strip_suffix('"')) + .unwrap_or(quoted) + .replace("\"\"", "\"") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn named(sql: &str, pick: fn(&DeclaredColumn) -> bool) -> Vec { + declared_columns(sql) + .into_iter() + .filter(pick) + .map(|column| column.name) + .collect() + } + + #[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!(named(sql, |column| column.generated), ["total", "odd, name"]); + } + + #[test] + fn a_collation_counts_only_on_the_column_itself() { + let sql = "CREATE TABLE c(code VARCHAR COLLATE NOCASE, \"Region\" VARCHAR COLLATE nocase.noaccent NOT NULL, \ + plain VARCHAR DEFAULT(('x' COLLATE NOCASE)), note VARCHAR, \ + CHECK((note COLLATE NOCASE = 'x')), FOREIGN KEY (code) REFERENCES p(code));"; + + let collations: Vec<(String, String)> = declared_columns(sql) + .into_iter() + .filter_map(|column| Some((column.name, column.collation?))) + .collect(); + + assert_eq!( + collations, + [ + ("code".to_owned(), "NOCASE".to_owned()), + ("Region".to_owned(), "nocase.noaccent".to_owned()) + ] + ); + } + + #[test] + fn a_table_with_none_has_none() { + let sql = "CREATE TABLE t(a INTEGER, b VARCHAR);"; + + assert!(named(sql, |column| column.generated || column.collation.is_some()).is_empty()); + assert_eq!(named(sql, |_| true), ["a", "b"]); + } +} 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..40baa0841a --- /dev/null +++ b/linux/crates/drivers/duckdb/src/catalog/mod.rs @@ -0,0 +1,694 @@ +//! 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 declared_columns; + +use std::collections::HashSet; + +use tablepro_core::column::{ColumnDefault, ColumnInfo, ColumnType, SqlExpression}; +use tablepro_core::ddl::{ + CollationName, ColumnAttributes, ColumnDefinition, GeneratedColumn, GeneratedStorage, IdentityStyle, + InboundForeignKey, OutboundForeignKey, 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::declared_columns::declared_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| { + declared_columns(&sql) + .into_iter() + .filter(|column| column.generated) + .map(|column| column.name) + .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, + partition_scoped: false, + 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()) +} + +/// The table's own keys, each with whether `=` on the referenced columns +/// finds the rows it matches: not when one of them is declared with a +/// collation, since DuckDB matches a key byte for byte. +pub(crate) fn outbound_foreign_keys( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result, DriverError> { + let keys = fetch_foreign_keys(link, catalog, table)?; + let mut outbound = Vec::with_capacity(keys.len()); + for key in keys { + let referenced = TableRef::new(key.ref_schema.clone(), key.ref_table.clone()); + let collated = collated_columns(link, catalog, &referenced)?; + let parent_compares_alike = !key + .ref_columns + .iter() + .any(|column| collated.iter().any(|name| name.eq_ignore_ascii_case(column))); + outbound.push(OutboundForeignKey { + key, + parent_compares_alike, + }); + } + Ok(outbound) +} + +/// The columns of a table declared with a collation, as its stored SQL +/// spells them. +fn collated_columns(link: &Link, catalog: &str, table: &TableRef) -> Result, DriverError> { + Ok(table_sql(link, catalog, table)? + .map(|sql| { + declared_columns(&sql) + .into_iter() + .filter(|column| column.collation.is_some()) + .map(|column| column.name) + .collect() + }) + .unwrap_or_default()) +} + +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. DuckDB matches a key +/// byte for byte and refuses one whose two sides differ in type, so `=` +/// on the referencing columns finds the rows the key matches unless one +/// of them is declared with a collation. +pub(crate) fn inbound_foreign_keys( + link: &Link, + catalog: &str, + table: &TableRef, +) -> Result, DriverError> { + let rows = text_rows( + link, + "SELECT c.constraint_name, c.schema_name, c.table_name, \ + array_to_string(c.constraint_column_names, chr(31)), \ + array_to_string(c.referenced_column_names, chr(31)), t.sql \ + FROM duckdb_constraints() c \ + JOIN duckdb_tables() t \ + ON t.database_name = c.database_name AND t.schema_name = c.schema_name AND t.table_name = c.table_name \ + WHERE c.database_name = ? AND c.schema_name = ? AND c.referenced_table = ? \ + AND c.constraint_type = 'FOREIGN KEY' \ + ORDER BY c.table_name, c.constraint_index", + &[catalog, schema_of(table), &table.name], + )?; + Ok(rows + .iter() + .map(|row| { + let columns = split_names(&text(row, 3)); + let collated: Vec = declared_columns(&text(row, 5)) + .into_iter() + .filter(|column| column.collation.is_some()) + .map(|column| column.name) + .collect(); + let compares_alike = !columns + .iter() + .any(|column| collated.iter().any(|name| name.eq_ignore_ascii_case(column))); + InboundForeignKey { + name: text(row, 0), + table: TableRef::new(Some(text(row, 1)), text(row, 2)), + child_compares_alike: compares_alike, + columns, + 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. Neither does a key +/// over a column declared with a collation: DuckDB keeps the key unique +/// byte for byte, while `=` and ORDER BY compare under the collation, so +/// `'ABC'` and `'abc'` are two rows one predicate finds. +pub(crate) fn row_identity(link: &Link, catalog: &str, table: &TableRef) -> Result { + let collated = collated_columns(link, catalog, table)?; + let compares_bytes = |columns: &[String]| { + !columns + .iter() + .any(|column| collated.iter().any(|name| name.eq_ignore_ascii_case(column))) + }; + let constraints = key_constraints(link, catalog, table)?; + if let Some((_, _, columns)) = constraints + .iter() + .find(|(_, kind, columns)| kind == "PRIMARY KEY" && compares_bytes(columns)) + { + 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, columns)| kind == "UNIQUE" && compares_bytes(columns)) + .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 collations: Vec<(String, String)> = create_sql + .as_deref() + .map(declared_columns) + .unwrap_or_default() + .into_iter() + .filter_map(|column| Some((column.name, column.collation?))) + .collect(); + 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 collation = collations + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(&info.name)) + .map(|(_, collation)| CollationName::new(None, collation.clone())); + let attributes = ColumnAttributes { + generated, + collation, + 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..37bf79f855 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/connection.rs @@ -0,0 +1,550 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, PoisonError}; + +use async_trait::async_trait; +use tablepro_core::ddl::{InboundForeignKey, OutboundForeignKey, 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, + given_up: CancellationToken, + ) -> Result { + let opening = endpoint.clone(); + let read_only = settings.read_only; + let (instance, version) = blocking(move || { + let instance = Instance::open(&opening, read_only, &given_up)?; + 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)?; + let catalog = instance.catalog().to_owned(); + Ok(Self { + dialect: Arc::new(DuckDbDialect::for_catalog(catalog.clone())), + catalog, + instance: Mutex::new(Some(instance)), + pool, + settings, + liveness, + server_info: server_info(&version), + 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).await + } + + /// A database that hit a fatal error refuses everything after it, so + /// the link is gone for good. A call that lost its connection says so + /// only when a connection of its own confirms it: one given up after a + /// stop that did not settle leaves the database as it was. + async fn watch(&self, result: Result) -> Result { + if matches!(result, Err(DriverError::ConnectionLost { .. })) + && !self.closed.load(Ordering::SeqCst) + && self.database_failed().await + { + self.link.mark_lost(); + } + result + } + + /// Whether the database refuses a new connection's `SELECT 1`, as it + /// does after a fatal error. A check that gets no answer within the + /// probe timeout proves nothing, and the caller is not held past it. + async fn database_failed(&self) -> bool { + let Ok(instance) = self.instance() else { + return false; + }; + let checking = blocking(move || batch(&instance.link()?, "SELECT 1")); + matches!( + tokio::time::timeout(self.liveness.probe_timeout, checking).await, + Ok(Err(DriverError::ConnectionLost { .. })) + ) + } + + /// 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.workspace_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).await + } + + async fn list_schemas(&self, options: CallOptions) -> Result, DriverError> { + self.read(options, catalog::list_schemas).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_outbound_foreign_keys( + &self, + table: &TableRef, + options: CallOptions, + ) -> Result, DriverError> { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::outbound_foreign_keys(link, catalog, &table) + }) + .await + } + + async fn fetch_inbound_foreign_keys( + &self, + table: &TableRef, + options: CallOptions, + ) -> Result, DriverError> { + let table = table.clone(); + self.read(options, move |link, catalog| { + catalog::inbound_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).await?; + // 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).await + } + + /// 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::*; + + /// A call given up after a stop that did not settle synthesizes a + /// lost connection, which leaves a healthy database's link live. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn a_lost_call_on_a_healthy_database_keeps_the_link() { + let directory = tempfile::tempdir().unwrap(); + let endpoint = FileEndpoint::create_new(directory.path().join("live.duckdb")).unwrap(); + let liveness = tablepro_test_support::conformance::test_liveness(); + let connection = DuckDbConnection::open( + &endpoint, + WorkspaceSettings::workspace(false), + liveness, + CancellationToken::new(), + ) + .await + .unwrap(); + + let lost: Result<(), DriverError> = Err(DriverError::ConnectionLost { + during: LossPhase::Statement, + }); + let _ = connection.watch(lost).await; + + assert_eq!(connection.link_state(), LinkState::Live); + connection.close().await.unwrap(); + } + + #[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..11faaae0dc --- /dev/null +++ b/linux/crates/drivers/duckdb/src/create_file.rs @@ -0,0 +1,254 @@ +use std::ffi::OsStr; +use std::fs::{File, OpenOptions}; +use std::io::ErrorKind; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; + +use rustix::fs::FlockOperation; +use rustix::io::Errno; +use tablepro_core::{BusyReason, DriverError}; +use tokio_util::sync::CancellationToken; + +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. +/// +/// `given_up` is the connect's stop, read again once the new database is +/// built: building it takes a while, and a connect given up meanwhile +/// leaves the old file where it is. +pub(crate) fn create_or_replace(path: &Path, given_up: &CancellationToken) -> 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(|()| match given_up.is_cancelled() { + true => Err(DriverError::Cancelled), + false => 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> { + match std::fs::metadata(path) { + Ok(metadata) if metadata.is_file() => {} + // 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 tablepro_core::held_by_this_process(path) { + 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)), + } +} + +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, &CancellationToken::new()).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, &CancellationToken::new()).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, &CancellationToken::new()), + Err(DriverError::Busy(BusyReason::FileLocked)) + ); + assert_eq!(std::fs::read(&path).unwrap(), b"open elsewhere in this process"); + drop(open); + } + + /// A connect given up while the new database was built leaves the + /// old file, and nothing half-built beside it. + #[test] + fn a_create_given_up_while_building_keeps_the_old_file() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("data.duckdb"); + std::fs::write(&path, b"what the user had").unwrap(); + let given_up = CancellationToken::new(); + given_up.cancel(); + + assert_eq!(create_or_replace(&path, &given_up), Err(DriverError::Cancelled)); + + assert_eq!(std::fs::read(&path).unwrap(), b"what the user had"); + let left: Vec<_> = std::fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + assert_eq!(left, [std::ffi::OsString::from("data.duckdb")]); + } + + #[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, &CancellationToken::new()), + 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..c42691b13b --- /dev/null +++ b/linux/crates/drivers/duckdb/src/ddl.rs @@ -0,0 +1,437 @@ +//! 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::{ColumnKind, ColumnType}; +use tablepro_core::ddl::ddl_sql::{column_list, create_index_sql, default_sql, foreign_key_sql}; +use tablepro_core::ddl::{ + BuildDdlError, ColumnAttributes, 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, + column_defaults: true, + required_columns: true, + declared_column_order: true, +}; + +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 qualified = self.qualify(&table); + let mut statements = vec![format!( + "CREATE TABLE {qualified} (\n {}\n)", + definitions.join(",\n ") + )]; + for column in &draft.columns { + if let Some(comment) = column.comment_text() { + statements.push(self.comment_on(&qualified, &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)?; + // DuckDB drops a column's collation with its old type + // unless the new one names it again. + let collation = collation_clause(Some(&alteration.original.attributes), &resolved.column_type); + statements.push(alter(format!("ALTER COLUMN {name} TYPE {}{collation}", 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(&qualified, &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(&qualified, &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(&self.portable_name(&definition.table), column.name(), Some(comment))); + } + } + Ok(DdlPlan::from_sql(statements)) + } +} + +/// ` COLLATE ` for a text column the catalogue read with one, as +/// DuckDB's own text spelled it. +fn collation_clause(attributes: Option<&ColumnAttributes>, column_type: &ColumnType) -> String { + attributes + .and_then(|attributes| attributes.collation.as_ref()) + .filter(|_| matches!(column_type.kind, ColumnKind::Text { .. })) + .map(|collation| format!(" COLLATE {}", collation.name)) + .unwrap_or_default() +} + +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), + format!( + "{}{}", + resolved.sql, + collation_clause(attributes, &resolved.column_type) + ), + ]; + 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(" ")) + } + + /// `table` as the statement names it: qualified for a change, portable + /// in a script the user copies to replay elsewhere. + fn comment_on(&self, table: &str, column: &str, comment: Option<&str>) -> String { + let target = format!("{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..5f04727449 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/dialect.rs @@ -0,0 +1,565 @@ +use tablepro_core::column::{ColumnInfo, ColumnKind, ColumnType, IntegerKind, IntegerWidth, ReadForm, SqlExpression}; +use tablepro_core::ddl::DdlDialect; +use tablepro_core::dialect::{ + BindError, BindTarget, DialectCapabilities, InsertConflict, KeysetDirection, LiteralError, Placeholder, SqlDialect, +}; +use tablepro_core::meta::{EngineRowId, EngineRowIdPart, TableRef}; +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::catalog::DEFAULT_SCHEMA; +use crate::duck_type::{column_type_from_text, holds_union}; +use crate::encode::duck_text; + +/// How DuckDB spells SQL. The tables of the file are named with its +/// catalogue: a schema can share a name with a catalogue, and DuckDB then +/// refuses `"sales"."t"` as ambiguous. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DuckDbDialect { + catalog: Option, +} + +impl DuckDbDialect { + pub fn new() -> Self { + Self::default() + } + + /// The spelling for the file DuckDB named `catalog`. + pub fn for_catalog(catalog: impl Into) -> Self { + Self { + catalog: Some(catalog.into()), + } + } + + 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, + insert_conflict: InsertConflict::Refused, + } + } + + fn grammar(&self) -> SqlGrammar { + SqlGrammar::DuckDb + } + + fn quote_identifier(&self, name: &str) -> String { + format!("\"{}\"", name.replace('"', "\"\"")) + } + + fn qualify(&self, table: &TableRef) -> String { + let Some(catalog) = &self.catalog else { + return self.portable_name(table); + }; + let schema = table + .schema + .as_deref() + .filter(|schema| !schema.is_empty()) + .unwrap_or(DEFAULT_SCHEMA); + format!( + "{}.{}.{}", + self.quote_identifier(catalog), + self.quote_identifier(schema), + self.quote_identifier(&table.name) + ) + } + + /// Without the catalogue: an export replays in a file of another name, + /// and DuckDB refuses a catalogue in a foreign key's REFERENCES. + fn portable_name(&self, table: &TableRef) -> String { + match table.schema.as_deref().filter(|schema| !schema.is_empty()) { + Some(schema) => format!( + "{}.{}", + self.quote_identifier(schema), + self.quote_identifier(&table.name) + ), + None => self.quote_identifier(&table.name), + } + } + + /// 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()), + // DuckDB reads `-0.0` as a decimal, which has no negative zero, + // so the sign survives only in the text of a cast. + Value::Float32(real) if *real == 0.0 && real.is_sign_negative() => Ok("CAST('-0.0' AS FLOAT)".to_owned()), + Value::Float64(real) if *real == 0.0 && real.is_sign_negative() => Ok("CAST('-0.0' AS DOUBLE)".to_owned()), + // 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 holds_union(&other.type_name) || holds_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. An ENUM label bound as +/// VARCHAR would compare as text, so a key read past `'sad'` in +/// `ENUM('sad', 'ok', 'happy')` would find nothing after it. A DATE's +/// infinities go as text too, which a row value such as `(d, id) > (?, ?)` +/// refuses to compare with a DATE. A UHUGEINT that fits 128 signed bits +/// binds as a HUGEINT, which DuckDB compares with a UHUGEINT by rounding. +fn needs_cast(column: &ColumnType) -> bool { + let unsigned_128 = matches!( + column.kind, + ColumnKind::Integer(IntegerKind { + width: IntegerWidth::W128, + signed: false, + }) + ); + unsigned_128 + || !matches!( + column.kind, + ColumnKind::Bool(_) + | ColumnKind::Integer(_) + | ColumnKind::Float(_) + | ColumnKind::Text { .. } + | ColumnKind::Bytes { .. } + | ColumnKind::Interval + ) +} + +/// 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)) +} + +/// 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, + partition_scoped: false, + default: ColumnDefault::None, + comment: None, + } + } + + #[test] + fn a_value_with_no_binding_is_cast_to_its_column() { + let dialect = DuckDbDialect::new(); + 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 an_unsigned_128_bit_value_is_cast_to_its_column() { + let unsigned = column("UHUGEINT"); + let signed = column("HUGEINT"); + + let bound = DuckDbDialect::new() + .placeholder(0, Value::Int(1), BindTarget::Column(&unsigned)) + .unwrap(); + let plain = DuckDbDialect::new() + .placeholder(0, Value::Int(1), BindTarget::Column(&signed)) + .unwrap(); + + assert_eq!(bound.sql, "CAST(? AS UHUGEINT)"); + assert_eq!(plain.sql, "?"); + } + + #[test] + fn an_enum_label_is_cast_to_its_column() { + let mood = column("ENUM('sad', 'ok', 'happy')"); + + let bound = DuckDbDialect::new() + .placeholder(0, Value::Text("sad".to_owned()), BindTarget::Column(&mood)) + .unwrap(); + + assert_eq!(bound.sql, "CAST(? AS ENUM('sad', 'ok', 'happy'))"); + } + + #[test] + fn a_table_is_named_with_the_files_catalogue_but_exported_without_it() { + let dialect = DuckDbDialect::for_catalog("sales"); + let table = TableRef::new(Some("sales".to_owned()), "t"); + + assert_eq!(dialect.qualify(&table), "\"sales\".\"sales\".\"t\""); + assert_eq!(dialect.qualify(&TableRef::unqualified("t")), "\"sales\".\"main\".\"t\""); + assert_eq!(dialect.portable_name(&table), "\"sales\".\"t\""); + } + + #[test] + fn a_union_value_has_no_literal() { + let dialect = DuckDbDialect::new(); + 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::new(); + 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::Float64(-0.0), &text).unwrap(), + "CAST('-0.0' AS DOUBLE)" + ); + assert_eq!( + dialect.literal(&Value::Float32(-0.0), &text).unwrap(), + "CAST('-0.0' AS FLOAT)" + ); + assert_eq!(dialect.literal(&Value::Bool(true), &text).unwrap(), "TRUE"); + } + + #[test] + fn a_nested_column_reads_as_duckdb_text() { + assert_eq!( + DuckDbDialect::new().projection(&column("INTEGER[]")), + "CAST(\"v\" AS VARCHAR) AS \"v\"" + ); + assert_eq!(DuckDbDialect::new().projection(&column("INTEGER")), "\"v\""); + assert_eq!( + DuckDbDialect::new().projection(&column("UNION(n INTEGER, s VARCHAR)[]")), + "\"v\"", + "a UNION's text names no member" + ); + } + + #[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::new().history_recording(sql), withheld, "{sql}"); + } + assert_eq!( + tablepro_test_support::history_cases::unrecorded_plain_statements(&DuckDbDialect::new()), + 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..f2726dbb74 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/driver.rs @@ -0,0 +1,63 @@ +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 driver id, which also names the engine in the process's file +/// claims (`tablepro_core::claim_file`). +pub(crate) const ENGINE: &str = "duckdb"; + +/// 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 { + ENGINE + } + + 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, + }); + }; + // The open runs on a blocking thread that outlives this call, so + // it learns of a cancel or a timeout from this token. + let given_up = CancellationToken::new(); + let _given_up_on_return = given_up.clone().drop_guard(); + let opening = DuckDbConnection::open(endpoint, options.workspace_settings, options.liveness, given_up); + 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..805d4e4379 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/duck_type.rs @@ -0,0 +1,597 @@ +//! 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, +}; +use tablepro_core::sql_syntax::SqlGrammar; +use tablepro_core::sql_syntax::script::{LexicalSettings, ScriptPlan, ScriptTokenKind}; + +/// 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(_)); + // DuckDB's text for a UNION names no member and reads back as the + // first member it fits, so a table tab reads one as a value of its + // type, which a SQL export has no literal for, rather than as text. + let read_form = match holds_union(declared) { + true => ReadForm::Native, + false => duck.read_form(), + }; + 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, + } +} + +/// Whether a spelled type holds a UNION, at any depth. A field may be +/// named `"union("`, so only the word itself before a parenthesis counts. +pub(crate) fn holds_union(text: &str) -> bool { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(text, grammar, LexicalSettings::default_for(grammar)); + let tokens: Vec<_> = plan + .tokens(0..text.len()) + .filter(|token| !token.kind.is_trivia()) + .collect(); + tokens.windows(2).any(|pair| { + pair[0].kind == ScriptTokenKind::Word + && text + .get(pair[0].range.clone()) + .is_some_and(|word| word.eq_ignore_ascii_case("UNION")) + && pair[1].kind == ScriptTokenKind::OpenParen + }) +} + +/// 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 a_union_is_found_at_any_depth_and_never_in_a_name() { + for text in [ + "UNION(n INTEGER, s VARCHAR)", + "union(n INTEGER)[]", + "STRUCT(a UNION(n INTEGER, s VARCHAR))", + "MAP(VARCHAR, UNION (n INTEGER))", + ] { + assert!(holds_union(text), "{text}"); + } + for text in ["STRUCT(\"union(\" INTEGER)", "VARCHAR", "STRUCT(\"a\" VARCHAR)"] { + assert!(!holds_union(text), "{text}"); + } + } + + #[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..b564c9398e --- /dev/null +++ b/linux/crates/drivers/duckdb/src/instance.rs @@ -0,0 +1,618 @@ +//! 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, FileClaim, FileEndpoint, FileOpenMode, LossPhase}; +use tokio_util::sync::CancellationToken; + +use crate::create_file::create_or_replace; +use crate::driver::ENGINE; +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, + /// The file and every file a tab attached, kept from SQLite until the + /// database closes. Dropped after `root`, once DuckDB has let go. + claims: Mutex>, +} + +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, + } + } + + 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. + /// `given_up` is the connect's own stop: the blocking open outlives a + /// caller that went while it waited, and must not then replace the + /// file. + pub(crate) fn open( + endpoint: &FileEndpoint, + read_only: bool, + given_up: &CancellationToken, + ) -> Result, DriverError> { + let path = endpoint.path(); + let _opening = tablepro_core::opening_file_blocking(); + 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, None)) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + if given_up.is_cancelled() { + return Err(DriverError::Cancelled); + } + create_or_replace(path, given_up)?; + } + (_, 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, None) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + // No DuckDB database here has the file, so a descriptor this + // process holds on it is SQLite's, whose locks DuckDB would + // drop by closing the file on finding no database of its own. + if tablepro_core::held_by_this_process(&canonical) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + let claim = tablepro_core::claim_file(&canonical, ENGINE)?; + 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, + claims: Mutex::new(claim.into_iter().collect()), + }); + 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())) + } + + /// A connection for the app's own reads and saves. `SET + /// default_collation` in an editor tab reaches every connection to + /// the database, and `=` on a key would then find `'ABC'` and `'abc'` + /// alike, which DuckDB keeps as two rows: these compare byte for byte + /// whatever the tabs set. + pub(crate) fn workspace_link(self: &Arc) -> Result { + let link = self.link()?; + link.connection + .execute_batch("SET SESSION default_collation = ''") + .map_err(map_error)?; + Ok(link) + } + + pub(crate) fn catalog(&self) -> &str { + &self.catalog + } +} + +/// The files a database has open, its own and the ones attached to it, +/// under the paths DuckDB lists them by. +fn database_paths(entry: &Opened) -> Vec { + let root = entry.root.lock().unwrap_or_else(PoisonError::into_inner); + let Some(root) = root.as_ref() else { + return Vec::new(); + }; + 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::, _>>()) + }); + paths.unwrap_or_else(|error| { + tracing::warn!(%error, "could not read the databases an open file has attached"); + Vec::new() + }) +} + +/// Whether an open database other than `except` has the file attached, +/// which DuckDB lists with its own file. +fn attached(open: &HashMap, id: FileId, except: Option) -> bool { + open.iter().filter(|(key, _)| Some(**key) != except).any(|(_, entry)| { + database_paths(entry) + .iter() + .any(|path| FileId::of(Path::new(path)) == Some(id)) + }) +} + +/// Run `attach`, an ATTACH on `link` that may open any of `targets`, +/// unless a database in this process has one of them open, as its own +/// file or attached to it: DuckDB would make a second database on it, +/// which writes past the first. DuckDB finds a file the link's own +/// database has open by the path it lists it under, so `ATTACH IF NOT +/// EXISTS` of that path again does nothing, while another name for the +/// file, a hard link among them, would open it twice. 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( + link: &Link, + targets: &[PathBuf], + attach: impl FnOnce() -> Result, +) -> Result { + let _opening = tablepro_core::opening_file_blocking(); + let open = OPEN.lock().unwrap_or_else(PoisonError::into_inner); + let own = link.instance.id; + let own_paths = open.get(&own).map(database_paths).unwrap_or_default(); + let in_use = targets.iter().any(|target| { + let Some(id) = FileId::of(target) else { + return false; + }; + let mut held_here = own_paths + .iter() + .filter(|path| FileId::of(Path::new(path)) == Some(id)) + .peekable(); + if held_here.peek().is_some() { + let spelled = target.to_string_lossy(); + return !held_here.any(|path| *path == spelled); + } + // A file no DuckDB database has that this process holds is + // SQLite's. + open.contains_key(&id) || attached(&open, id, Some(own)) || tablepro_core::held_by_this_process(target) + }); + if in_use { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } + // A FIFO would hold the ATTACH, and every open in the process behind + // it, until something wrote to it. + if let Some(target) = targets + .iter() + .find(|target| std::fs::metadata(target).is_ok_and(|metadata| !metadata.is_file())) + { + return Err(DriverError::NotADatabase { path: target.clone() }); + } + let result = attach()?; + // `ATTACH IF NOT EXISTS` under an alias already taken opens nothing, + // so only a file the database now lists is claimed. + let listed: Vec = open + .get(&own) + .map(database_paths) + .unwrap_or_default() + .iter() + .filter_map(|path| FileId::of(Path::new(path))) + .collect(); + let mut claims = Vec::with_capacity(targets.len()); + for target in targets { + if FileId::of(target).is_some_and(|id| listed.contains(&id)) { + claims.extend(tablepro_core::claim_file(target, ENGINE)?); + } + } + link.instance + .claims + .lock() + .unwrap_or_else(PoisonError::into_inner) + .extend(claims); + drop(open); + Ok(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. +/// A directory, a FIFO or a device is no database, and opening a FIFO +/// would wait for a writer that never comes. +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_file() => 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, + &CancellationToken::new(), + ); + + 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, + &CancellationToken::new(), + ) + .unwrap(); + let again = FileEndpoint::open_existing(directory.path().join("one.duckdb")).unwrap(); + + let second = Instance::open(&again, false, &CancellationToken::new()).unwrap(); + + assert!(Arc::ptr_eq(&first, &second)); + assert_eq!( + Instance::open(&again, true, &CancellationToken::new()).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, + &CancellationToken::new(), + ) + .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, + &CancellationToken::new(), + ) + .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, + &CancellationToken::new() + ) + .is_ok(), + "the file was still held once every connection was gone" + ); + } + + /// A connect given up while its open waited does not go on to + /// replace the file. + #[test] + fn a_given_up_create_leaves_the_file_alone() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("kept.duckdb"); + std::fs::write(&path, b"what the user had").unwrap(); + let given_up = CancellationToken::new(); + given_up.cancel(); + + let opened = Instance::open(&FileEndpoint::create_new(path.clone()).unwrap(), false, &given_up); + + assert_eq!(opened.unwrap_err(), DriverError::Cancelled); + assert_eq!(std::fs::read(&path).unwrap(), b"what the user had"); + } + + /// SQLite holding the file in this process would lose its locks when + /// DuckDB closed the file again. + #[test] + fn a_file_another_engine_holds_is_not_opened() { + let directory = tempfile::tempdir().unwrap(); + let endpoint = created(directory.path(), "held.duckdb"); + drop(Instance::open(&endpoint, false, &CancellationToken::new()).unwrap()); + let reopen = FileEndpoint::open_existing(directory.path().join("held.duckdb")).unwrap(); + let held = std::fs::File::open(directory.path().join("held.duckdb")).unwrap(); + + let opened = Instance::open(&reopen, false, &CancellationToken::new()); + + assert_eq!(opened.unwrap_err(), DriverError::Busy(BusyReason::FileLocked)); + drop(held); + let instance = Instance::open(&reopen, false, &CancellationToken::new()).unwrap(); + assert!(tablepro_core::claimed_by_another(&instance.path, "sqlite")); + } + + #[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, &CancellationToken::new()).unwrap(); + + assert_eq!( + Instance::open(&endpoint, false, &CancellationToken::new()).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, + &CancellationToken::new(), + ) + .unwrap(); + + assert_eq!(instance.catalog(), "sales"); + assert!(!instance.read_only); + } + + /// Opening a FIFO would wait for a writer, with every other open in + /// the process waiting behind it. + #[test] + fn a_fifo_is_no_database() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("pipe.duckdb"); + assert!( + std::process::Command::new("mkfifo") + .arg(&path) + .status() + .unwrap() + .success() + ); + + let opened = Instance::open( + &FileEndpoint::open_existing(path.clone()).unwrap(), + true, + &CancellationToken::new(), + ); + + assert_eq!(opened.unwrap_err(), DriverError::NotADatabase { path }); + } + + #[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, + &CancellationToken::new(), + ); + + 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..c3d77c0c27 --- /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) => crate::connection::blocking(move || instance.workspace_link()).await, + 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..48d2c3f638 --- /dev/null +++ b/linux/crates/drivers/duckdb/src/result_stream.rs @@ -0,0 +1,833 @@ +//! 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, ScriptDiagnostic, ScriptPlan, ScriptToken, ScriptTokenKind}; +use tablepro_core::statement::{BoundParam, Statement}; +use tablepro_core::value::Value; +use tablepro_core::{DriverError, QueryResult, ResultEvent, RowLimit, Truncation, UnsupportedFeature}; +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) => { + // DuckDB leaves a HIDDEN database out of what it lists, so + // no check could tell another window it is open. + if attach_hides(statement) { + return Err(DriverError::Unsupported { + feature: UnsupportedFeature::HiddenAttach, + }); + } + let targets = attach_targets(link, literal)?; + let ran = attach_unless_open(link, &targets, || { + // A stop that came while the ATTACH waited its turn: + // DuckDB clears an interrupt as a statement starts. + if halt() { + return Ok(false); + } + run_statement(link, statement, &[], limit, events, &mut outcome).map(|()| true) + })?; + if !ran { + outcome.halted = true; + break; + } + } + 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); + } + // A CTE may be named `update`, but its name follows WITH, RECURSIVE + // or a comma, while the statement follows the last body's `)`. + let mut after_body = false; + for token in code { + match token.kind { + ScriptTokenKind::OpenParen => depth += 1, + ScriptTokenKind::CloseParen => { + depth = depth.saturating_sub(1); + after_body = depth == 0; + continue; + } + ScriptTokenKind::Word if depth == 0 && after_body => { + let candidate = word(token.range)?; + if VERBS.contains(&candidate.as_str()) { + return Some(candidate); + } + } + _ => {} + } + if depth == 0 { + after_body = false; + } + } + 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| resolved_like_duckdb(expand_path(path, home.as_deref()))) + .collect()) +} + +/// The path as DuckDB opens it: made absolute, each part that is there +/// resolved with its links, and `..` taken by its text past one that is +/// not, so `d/missing/../a.duckdb` is `d/a.duckdb` and `link/../a.duckdb` +/// is beside where the link points. +fn resolved_like_duckdb(path: PathBuf) -> PathBuf { + use std::path::Component; + let absolute = match path.is_absolute() { + true => path, + false => std::env::current_dir().map_or(path.clone(), |directory| directory.join(&path)), + }; + let mut resolved = PathBuf::new(); + for component in absolute.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + resolved.pop(); + } + other => { + resolved.push(other); + if let Ok(canonical) = std::fs::canonicalize(&resolved) { + resolved = canonical; + } + } + } + } + resolved +} + +/// Whether an ATTACH asks for a HIDDEN database, which DuckDB leaves out +/// of `duckdb_databases()`. Its options are the parenthesis after the +/// path, each a name, quoted or not, and an optional value; measured on +/// DuckDB 1.5, `HIDDEN false` attaches a listed database. +fn attach_hides(statement: &str) -> bool { + let grammar = SqlGrammar::DuckDb; + let plan = ScriptPlan::build(statement, grammar, LexicalSettings::default_for(grammar)); + let tokens: Vec<(ScriptTokenKind, &str)> = plan + .tokens(0..statement.len()) + .filter(|token| !token.kind.is_trivia()) + .map(|token| (token.kind, statement.get(token.range).unwrap_or_default())) + .collect(); + let mut depth = 0usize; + let mut option_starts = false; + for (index, &(kind, text)) in tokens.iter().enumerate() { + let is_option_name = option_starts; + option_starts = false; + match kind { + ScriptTokenKind::OpenParen => { + depth += 1; + option_starts = depth == 1; + } + ScriptTokenKind::CloseParen => depth = depth.saturating_sub(1), + ScriptTokenKind::Punct if depth == 1 && text == "," => option_starts = true, + ScriptTokenKind::Word | ScriptTokenKind::QuotedIdentifier if is_option_name => { + let name = text + .strip_prefix('"') + .and_then(|name| name.strip_suffix('"')) + .unwrap_or(text); + let turned_off = tokens.get(index + 1).is_some_and(|&(kind, value)| { + let value = match kind { + ScriptTokenKind::StringLiteral => value.trim_matches('\''), + _ => value, + }; + value.eq_ignore_ascii_case("false") || value == "0" + }); + if name.eq_ignore_ascii_case("hidden") && !turned_off { + return true; + } + } + _ => {} + } + } + false +} + +/// 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, identifier or block comment runs to the end +/// of the text, and the statement it opens or continues goes to DuckDB as +/// it stands, so the error is DuckDB's own. Cutting an open comment off +/// would run what came before it: `DELETE FROM t /* where ...` would +/// empty the table. +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_at = plan + .diagnostics() + .iter() + .map(|ScriptDiagnostic::Unterminated { start, .. }| *start) + .min(); + let Some(open_at) = open_at else { + return plan.statements().iter().map(|statement| statement.text(sql)).collect(); + }; + let mut statements = Vec::with_capacity(plan.statements().len()); + let mut tail_start = open_at; + for statement in plan.statements() { + if statement.range.start >= open_at { + break; + } + let ended = statement.range.end <= open_at + && plan + .tokens(statement.range.end..open_at) + .any(|token| token.kind == ScriptTokenKind::Semicolon); + if !ended { + tail_start = statement.range.start; + break; + } + statements.push(statement.text(sql)); + } + let tail = sql.get(tail_start..).unwrap_or_default().trim_end(); + if !tail.is_empty() { + statements.push(tail); + } + 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", "/* open"]); + assert_eq!( + split_statements("SELECT 1; DELETE FROM t /* WHERE a = 1"), + ["SELECT 1", "DELETE FROM t /* WHERE a = 1"] + ); + assert_eq!( + split_statements("DELETE FROM t -- why;\n/* open"), + ["DELETE FROM t -- why;\n/* open"] + ); + assert!(split_statements(" -- only a comment\n").is_empty()); + } + + #[test] + fn a_cte_named_like_a_verb_is_not_the_statement() { + assert_eq!( + main_verb("WITH update AS (SELECT true AS \"Success\") SELECT * FROM update").as_deref(), + Some("SELECT") + ); + assert_eq!( + main_verb("WITH RECURSIVE insert (n) AS (SELECT 1), delete AS MATERIALIZED (SELECT 2) DELETE FROM t") + .as_deref(), + Some("DELETE") + ); + assert_eq!( + main_verb("WITH src AS (SELECT 1 AS a) INSERT INTO t SELECT a FROM src").as_deref(), + Some("INSERT") + ); + } + + #[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")); + } + + /// Measured against DuckDB 1.5: a missing part is passed by its text, + /// a link is followed before the `..` after it. + #[test] + fn a_path_is_resolved_as_duckdb_opens_it() { + let root = tempfile::tempdir().unwrap(); + let base = std::fs::canonicalize(root.path()).unwrap(); + std::fs::create_dir_all(base.join("srv/sub")).unwrap(); + std::fs::create_dir(base.join("links")).unwrap(); + std::os::unix::fs::symlink(base.join("srv/sub"), base.join("links/sub")).unwrap(); + + assert_eq!( + resolved_like_duckdb(base.join("missing/../held.duckdb")), + base.join("held.duckdb") + ); + assert_eq!( + resolved_like_duckdb(base.join("links/sub/../x.duckdb")), + base.join("srv/x.duckdb") + ); + assert_eq!( + resolved_like_duckdb(base.join("./srv/./a.duckdb")), + base.join("srv/a.duckdb") + ); + assert!(resolved_like_duckdb(PathBuf::from("rel.duckdb")).is_absolute()); + } + + /// Measured against DuckDB 1.5: a quoted option name is read as the + /// bare one, and `false` turns the option off. + #[test] + fn a_hidden_attach_is_told_by_its_options() { + assert!(attach_hides("ATTACH 'a.duckdb' AS a (HIDDEN)")); + assert!(attach_hides("ATTACH 'a.duckdb' (READ_ONLY, hidden true)")); + assert!(attach_hides("ATTACH 'a.duckdb' AS a (\"hidden\")")); + assert!(attach_hides("ATTACH 'a.duckdb' AS a (\"read_only\" false,hidden)")); + assert!(!attach_hides("ATTACH 'a.duckdb' AS a (HIDDEN false)")); + assert!(!attach_hides("ATTACH 'a.duckdb' AS a (TYPE duckdb, HIDDEN 0)")); + assert!(!attach_hides("ATTACH 'a.duckdb' AS a (HIDDEN 'false')")); + assert!(!attach_hides("ATTACH 'a.duckdb' AS hidden")); + assert!(!attach_hides("ATTACH 'hidden.duckdb' AS a (READ_ONLY)")); + assert!(!attach_hides("ATTACH 'a.duckdb' AS a (TYPE hidden)")); + } + + #[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..71eb234115 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/catalog.rs @@ -0,0 +1,434 @@ +//! 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(); +} + +/// An inbound key pairs its columns in declared order, and is read alone +/// as the definition lists it. DuckDB matches a key byte for byte, so a +/// column on either side declared with a collation finds other rows with +/// `=` than the key matches. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn inbound_keys_pair_their_columns_and_say_how_they_compare() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE customers (id INTEGER, region VARCHAR, code VARCHAR COLLATE NOCASE UNIQUE, \ + PRIMARY KEY (region, id))", + "CREATE TABLE orders (id INTEGER PRIMARY KEY, buyer INTEGER, buyer_region VARCHAR, \ + FOREIGN KEY (buyer_region, buyer) REFERENCES customers (region, id))", + "CREATE TABLE by_code (code VARCHAR REFERENCES customers (code))", + "CREATE TABLE by_region (id INTEGER, \"Region\" VARCHAR COLLATE NOCASE, \ + FOREIGN KEY (\"Region\", id) REFERENCES customers (region, id))", + "INSERT INTO customers VALUES (1, 'eu', 'ABC'), (1, 'EU', 'abc')", + "INSERT INTO by_code VALUES ('abc')", + "INSERT INTO by_region VALUES (1, 'EU')", + ], + ) + .await + .unwrap(); + let customers = TableRef::unqualified("customers"); + + let inbound = connection.fetch_inbound_foreign_keys(&customers, call()).await.unwrap(); + let definition = connection.fetch_table_definition(&customers, call()).await.unwrap(); + let by_region = query( + connection.as_ref(), + "SELECT count(*) FROM by_region WHERE \"Region\" = 'eu'", + ) + .await; + let by_code = query(connection.as_ref(), "SELECT count(*) FROM by_code WHERE code = 'ABC'").await; + + let keys: Vec<(String, Vec, Vec, bool)> = inbound + .iter() + .map(|key| { + ( + key.table.name.clone(), + key.columns.clone(), + key.referenced_columns.clone(), + key.child_compares_alike, + ) + }) + .collect(); + let key = |table: &str, columns: &[&str], referenced: &[&str], alike: bool| { + let owned = |names: &[&str]| names.iter().map(|name| (*name).to_owned()).collect::>(); + (table.to_owned(), owned(columns), owned(referenced), alike) + }; + assert_eq!( + keys, + [ + key("by_code", &["code"], &["code"], true), + key("by_region", &["Region", "id"], &["region", "id"], false), + key("orders", &["buyer_region", "buyer"], &["region", "id"], true), + ] + ); + assert_eq!(inbound[0].table.schema.as_deref(), Some("main")); + assert_eq!(definition.inbound_foreign_keys, inbound); + assert_eq!( + by_region.rows, + vec![vec![Value::Int(1)]], + "the collated column finds a row the key does not point at" + ); + assert_eq!( + by_code.rows, + vec![vec![Value::Int(0)]], + "the plain column finds only the rows the key points at" + ); + let mut outbound = Vec::new(); + for table in ["by_code", "by_region", "orders"] { + let keys = connection + .fetch_outbound_foreign_keys(&TableRef::unqualified(table), call()) + .await + .unwrap(); + outbound.extend(keys.into_iter().map(|key| (table, key.parent_compares_alike))); + } + assert_eq!( + outbound, + [("by_code", false), ("by_region", true), ("orders", true)], + "a key to a collated column finds other rows with `=` there than it matches" + ); + for refused in [ + "INSERT INTO by_code VALUES ('aBc')", + "CREATE TABLE by_wide_id (id BIGINT, region VARCHAR, FOREIGN KEY (region, id) REFERENCES customers (region, id))", + ] { + assert!(apply(connection.as_ref(), &[refused]).await.is_err(), "{refused}"); + } + 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, and +/// neither does a key over a collated column, which DuckDB keeps unique +/// byte for byte while `=` compares under the collation. +#[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)", + "CREATE TABLE folded (code VARCHAR COLLATE NOCASE PRIMARY KEY, id INTEGER NOT NULL UNIQUE)", + "CREATE TABLE folded_only (\"Code\" VARCHAR COLLATE NOCASE PRIMARY KEY)", + "INSERT INTO folded_only VALUES ('ABC'), ('abc')", + ], + ) + .await + .unwrap(); + let both = query( + connection.as_ref(), + "SELECT count(*) FROM folded_only WHERE \"Code\" = 'abc'", + ) + .await; + let mut identities = Vec::new(); + for name in ["keyed", "coded", "loose", "folded", "folded_only"] { + 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, + RowIdentity::UniqueKey { + columns: vec!["id".to_owned()] + }, + RowIdentity::Unordered, + ] + ); + assert_eq!(both.rows, vec![vec![Value::Int(2)]]); + connection.close().await.unwrap(); +} + +/// Why a table without a key is read-only: a checkpoint that compacts +/// row groups hands the rows new rowids. +/// `SET default_collation` in a tab reaches every connection to the +/// database, but the grid's reads keep comparing a key byte for byte, so +/// `'ABC'` and `'abc'` stay two rows to them. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_tabs_default_collation_leaves_the_grids_keys_alone() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE k (code VARCHAR PRIMARY KEY)", + "INSERT INTO k VALUES ('ABC'), ('abc')", + ], + ) + .await + .unwrap(); + let editor = session(connection.as_ref()).await; + + run(editor.as_ref(), "SET default_collation = 'nocase'").await; + let (_, in_tab) = run(editor.as_ref(), "SELECT count(*) FROM k WHERE code = 'abc'").await; + let in_grid = query(connection.as_ref(), "SELECT count(*) FROM k WHERE code = 'abc'").await; + + assert_eq!(in_tab.rows(), vec![vec![Value::Int(2)]], "the setting took"); + assert_eq!(in_grid.rows, vec![vec![Value::Int(1)]]); + editor.close().await; + connection.close().await.unwrap(); +} + +#[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(); +} + +/// A schema can share its name with the file's catalogue, and DuckDB +/// refuses `"test"."t"` as ambiguous then, so the app names a table with +/// the catalogue as well, and its structure changes run. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_schema_named_like_the_file_is_read_and_changed() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE SCHEMA test", + "CREATE TABLE test.test.t (id INTEGER PRIMARY KEY, a INTEGER)", + "INSERT INTO test.test.t VALUES (1, 2)", + ], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let table = TableRef::new(Some("test".to_owned()), "t"); + let original = connection.fetch_table_definition(&table, call()).await.unwrap(); + let mut changed = tablepro_core::ddl::TableDraft::from_definition(&original); + changed.columns.push(tablepro_core::ddl::DraftColumn::new( + "b", + tablepro_core::ddl::DraftType::Typed("INTEGER".to_owned()), + )); + + let read = query( + connection.as_ref(), + &format!("SELECT a FROM {}", dialect.qualify(&table)), + ) + .await; + let plan = dialect + .ddl() + .unwrap() + .plan_alter_table(&original, &tablepro_core::ddl::diff_table(&original, &changed)) + .unwrap(); + connection + .write_batch(&plan.into_write_batch(table.clone()), call()) + .await + .unwrap(); + let columns = connection.fetch_columns(&table, call()).await.unwrap(); + + assert_eq!(read.rows, vec![vec![Value::Int(2)]]); + assert_eq!(columns.len(), 3); + assert_eq!(dialect.portable_name(&table), "\"test\".\"t\""); + 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..07369a8788 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/conformance_engine.rs @@ -0,0 +1,20 @@ +//! 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, + server_stop: true, + 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..65883bc6bc --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/ddl.rs @@ -0,0 +1,284 @@ +//! 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(); +} + +/// DuckDB drops a column's collation with its old type unless the change +/// names it again, so a new type keeps the collation the column had. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_new_type_keeps_the_columns_collation() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &["CREATE TABLE t (code VARCHAR COLLATE nocase.noaccent, n INTEGER)"], + ) + .await + .unwrap(); + let dialect = connection.dialect(); + let ddl = dialect.ddl().unwrap(); + let original = definition(connection.as_ref(), "t").await; + let mut retyped = TableDraft::from_definition(&original); + retyped.columns[0].column_type = DraftType::Typed("TEXT".to_owned()); + + let plan = ddl + .plan_alter_table(&original, &diff_table(&original, &retyped)) + .unwrap(); + connection + .write_batch(&plan.into_write_batch(TableRef::unqualified("t")), call()) + .await + .unwrap(); + let stored = query( + connection.as_ref(), + "SELECT sql FROM duckdb_tables() WHERE table_name = 't'", + ) + .await; + + assert_eq!( + original.columns[0] + .attributes + .collation + .as_ref() + .map(|collation| collation.name.as_str()), + Some("nocase.noaccent") + ); + assert!( + matches!(&stored.rows[0][0], Value::Text(sql) if sql.contains("code VARCHAR COLLATE nocase.noaccent")), + "{:?}", + stored.rows + ); + 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..991046e01a --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/decode.rs @@ -0,0 +1,164 @@ +//! Values read off DuckDB's results, checked against DuckDB's own text +//! for them. + +use tablepro_core::RunEnd; +use tablepro_core::meta::TableRef; +use tablepro_core::value::{OtherValue, Value}; + +use crate::support::{TestFile, apply, call, query, 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(); +} + +/// A table tab reads a UNION, or a type holding one, as a value of its +/// type rather than as DuckDB's text, which names no member: a SQL export +/// then has no literal for it instead of writing text that reads back as +/// another member. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_union_in_a_table_reads_as_a_value_with_no_literal() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE u (id INTEGER PRIMARY KEY, v UNION(n INTEGER, s VARCHAR), \ + l UNION(n INTEGER, s VARCHAR)[], st STRUCT(a UNION(n INTEGER, s VARCHAR)))", + "INSERT INTO u VALUES (1, union_value(n := 2), [union_value(s := 'x')], {'a': union_value(n := 3)})", + ], + ) + .await + .unwrap(); + let columns = connection + .fetch_columns(&TableRef::unqualified("u"), call()) + .await + .unwrap(); + let dialect = connection.dialect(); + let projected: Vec = columns.iter().map(|column| dialect.projection(column)).collect(); + + let read = query(connection.as_ref(), &format!("SELECT {} FROM u", projected.join(", "))).await; + + for (index, column) in columns.iter().enumerate().skip(1) { + let value = &read.rows[0][index]; + assert!(matches!(value, Value::Other(_)), "{}: {value:?}", column.name); + assert!( + dialect.literal(value, &read.columns[index].column_type).is_err(), + "{} has a literal: {value:?}", + column.name + ); + } + 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/filter.rs b/linux/crates/drivers/duckdb/tests/integration/filter.rs new file mode 100644 index 0000000000..b7e187dcfa --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/filter.rs @@ -0,0 +1,99 @@ +//! The table filter on DuckDB's own types. + +use tablepro_core::filter::{FilterOp, FilterRule, FilterSet, FilterValue, build_filter}; +use tablepro_core::meta::TableRef; +use tablepro_core::statement::StatementBuilder; +use tablepro_core::{Connection, RowLimit, Value}; + +use crate::support::{TestFile, apply, call}; + +async fn ids_where(connection: &dyn Connection, column: &str, op: FilterOp, text: &str) -> Vec { + let table = TableRef::unqualified("docs"); + let columns = connection.fetch_columns(&table, call()).await.unwrap(); + let dialect = connection.dialect(); + let mut builder = StatementBuilder::new(); + builder.push_sql(&format!("SELECT id FROM {} WHERE ", dialect.qualify(&table))); + let filter = FilterSet { + rules: vec![FilterRule { + column: column.to_owned(), + op, + value: Some(FilterValue::Single(text.to_owned())), + }], + ..FilterSet::default() + }; + build_filter(&mut builder, dialect.as_ref(), &columns, &filter).unwrap(); + builder.push_sql(" ORDER BY id"); + connection + .query_bound(&builder.finish(), RowLimit::EDITOR_DEFAULT, call()) + .await + .unwrap() + .rows + .into_iter() + .filter_map(|row| row.into_iter().next()) + .collect() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn each_type_filters_on_the_text_the_grid_shows() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TYPE mood AS ENUM ('calm', 'cross')", + "CREATE TABLE docs (id INTEGER PRIMARY KEY, b BLOB, j JSON, m mood, u UUID, h HUGEINT, made TIMESTAMP)", + "INSERT INTO docs VALUES \ + (1, '\\x0A\\xFF'::BLOB, '{\"tag\":\"red\"}', 'calm', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', \ + 170141183460469231731687303715884105727, TIMESTAMP '2024-01-01 10:00:00'), \ + (2, '\\x0A'::BLOB, '{\"tag\":\"blue\"}', 'cross', '00000000-0000-0000-0000-000000000000', \ + -1, TIMESTAMP '2025-06-01 10:00:00')", + ], + ) + .await + .unwrap(); + let first = || vec![Value::Int(1)]; + let second = || vec![Value::Int(2)]; + + assert_eq!( + ids_where(connection.as_ref(), "b", FilterOp::Eq, "0x0AFF").await, + first() + ); + assert_eq!( + ids_where(connection.as_ref(), "b", FilterOp::NotEq, "0x0aff").await, + second() + ); + assert_eq!( + ids_where(connection.as_ref(), "j", FilterOp::Contains, "red").await, + first() + ); + assert_eq!(ids_where(connection.as_ref(), "m", FilterOp::Eq, "calm").await, first()); + assert_eq!( + ids_where(connection.as_ref(), "m", FilterOp::Contains, "ros").await, + second() + ); + assert_eq!( + ids_where( + connection.as_ref(), + "u", + FilterOp::Eq, + "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11" + ) + .await, + first() + ); + assert_eq!( + ids_where( + connection.as_ref(), + "h", + FilterOp::Eq, + "170141183460469231731687303715884105727" + ) + .await, + first() + ); + assert_eq!( + ids_where(connection.as_ref(), "made", FilterOp::Gt, "2024-12-31 00:00:00").await, + second() + ); + connection.close().await.unwrap(); +} 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..ed176da7d3 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/main.rs @@ -0,0 +1,30 @@ +//! 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 filter; +#[cfg(test)] +mod open_semantics; +#[cfg(test)] +mod paging; +#[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..931fd8704f --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/open_semantics.rs @@ -0,0 +1,442 @@ +//! 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 (attaching_again, _) = run( + editor.as_ref(), + &format!("ATTACH IF NOT EXISTS '{}' AS s", spare.path.display()), + ) + .await; + let unrelated = TestFile::new(); + let (no_op, _) = run( + editor.as_ref(), + &format!("ATTACH IF NOT EXISTS '{}' AS s", unrelated.path.display()), + ) + .await; + let spare_link = spare.path.with_file_name("spare-link.duckdb"); + std::fs::hard_link(&spare.path, &spare_link).unwrap(); + let own_link = file.path.with_file_name("own-link.duckdb"); + std::fs::hard_link(&file.path, &own_link).unwrap(); + let mut through_links = Vec::new(); + for (alias, link) in [("s2", &spare_link), ("me", &own_link)] { + let (summary, _) = run( + editor.as_ref(), + &format!("ATTACH IF NOT EXISTS '{}' AS {alias}", link.display()), + ) + .await; + through_links.push(summary.end); + } + let opening_attached = connect(spare.endpoint(), writable()).await; + // DuckDB follows a link before it looks for the file among its own, + // so a link to one it has open is refused by DuckDB itself. + let spare_symlink = spare.path.with_file_name("spare-symlink.duckdb"); + std::os::unix::fs::symlink(&spare.path, &spare_symlink).unwrap(); + let (through_symlink, _) = run(editor.as_ref(), &format!("ATTACH '{}' AS s3", spare_symlink.display())).await; + let hidden = TestFile::new(); + let (attaching_hidden, _) = run( + editor.as_ref(), + &format!("ATTACH '{}' AS h (HIDDEN)", hidden.path.display()), + ) + .await; + + assert_eq!( + attaching_open.end, + RunEnd::Failed(DriverError::Busy(BusyReason::FileLocked)) + ); + assert_eq!(attaching_spare.end, RunEnd::Completed); + assert!( + matches!(&through_symlink.end, RunEnd::Failed(DriverError::Server(diagnostics)) if diagnostics.message.contains("already attached")), + "{:?}", + through_symlink.end + ); + assert_eq!( + attaching_again.end, + RunEnd::Completed, + "the tab's own database has the file, which DuckDB finds itself" + ); + assert_eq!(no_op.end, RunEnd::Completed); + assert!( + !tablepro_core::claimed_by_another(&unrelated.path, "sqlite"), + "an ATTACH under a taken alias opened nothing to claim" + ); + assert!(tablepro_core::claimed_by_another(&spare.path, "sqlite")); + assert_eq!( + through_links, + [ + RunEnd::Failed(DriverError::Busy(BusyReason::FileLocked)), + RunEnd::Failed(DriverError::Busy(BusyReason::FileLocked)) + ], + "DuckDB finds its own files by path, so a hard link would open one twice" + ); + assert_eq!(opening_attached.err(), Some(DriverError::Busy(BusyReason::FileLocked))); + assert_eq!( + attaching_hidden.end, + RunEnd::Failed(DriverError::Unsupported { + feature: tablepro_core::UnsupportedFeature::HiddenAttach + }), + "DuckDB lists a HIDDEN database nowhere another window could see it" + ); + assert!(!tablepro_core::held_by_this_process(&hidden.path)); + 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 and a `\u` escape 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 '{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"), + format!("ATTACH '{directory}/missing/../{name}' 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".to_owned(), + format!("ATTACH U&'{path}' AS o"), + format!("ATTACH E'{}' AS o", path.replace('/', "\\u002f")), + ] { + 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(); +} + +/// An ATTACH of a FIFO would wait for a writer, with every open in the +/// process waiting behind it. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_attach_of_a_fifo_is_refused() { + let file = TestFile::new(); + let connection = file.open().await; + let editor = session(connection.as_ref()).await; + let fifo = file.path.with_file_name("pipe.duckdb"); + assert!( + std::process::Command::new("mkfifo") + .arg(&fifo) + .status() + .unwrap() + .success() + ); + + let (attached, _) = run( + editor.as_ref(), + &format!("ATTACH '{}' AS p (READ_ONLY)", fifo.display()), + ) + .await; + + assert_eq!(attached.end, RunEnd::Failed(DriverError::NotADatabase { path: fifo })); + editor.close().await; + connection.close().await.unwrap(); +} diff --git a/linux/crates/drivers/duckdb/tests/integration/paging.rs b/linux/crates/drivers/duckdb/tests/integration/paging.rs new file mode 100644 index 0000000000..9e3b9857d2 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/paging.rs @@ -0,0 +1,133 @@ +//! Paging a table the way the browse tab does, past the edge row's key. + +use tablepro_core::browse::{PageRead, PageRequest, read_page}; +use tablepro_core::filter::FilterSet; +use tablepro_core::meta::{PagePosition, RowIdentity, TableRef}; +use tablepro_core::{ColumnInfo, Connection, Value}; + +use crate::support::{TestFile, apply, call}; + +type Failure = Box; + +struct Table { + table: TableRef, + columns: Vec, + identity: RowIdentity, +} + +impl Table { + async fn read(connection: &dyn Connection, name: &str) -> Self { + let table = TableRef::unqualified(name); + Self { + columns: connection.fetch_columns(&table, call()).await.unwrap(), + identity: connection.row_identity(&table, call()).await.unwrap(), + table, + } + } + + /// One row per page, so every page is read past a key. + async fn page(&self, connection: &dyn Connection, position: &PagePosition) -> (PageRead, Vec) { + let filter = FilterSet::default(); + let request = PageRequest { + table: &self.table, + columns: &self.columns, + filter: &filter, + sort: &[], + identity: &self.identity, + position, + limit: 1, + }; + let read = read_page::(connection, &request, &call()).await.unwrap(); + let rows = read.result.rows.iter().map(|row| row[0].clone()).collect(); + (read, rows) + } +} + +/// An ENUM key sorts in the order its labels are declared, and the key a +/// page is read past compares the same way, forward and back. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn an_enum_key_pages_in_declared_order() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", + "CREATE TABLE moods (m mood PRIMARY KEY)", + "INSERT INTO moods VALUES ('happy'), ('sad'), ('ok')", + ], + ) + .await + .unwrap(); + let table = Table::read(connection.as_ref(), "moods").await; + let declared: Vec = ["sad", "ok", "happy"] + .into_iter() + .map(|label| Value::Text(label.to_owned())) + .collect(); + + let mut forward = Vec::new(); + let mut position = PagePosition::First; + for _ in 0..declared.len() + 1 { + let (read, rows) = table.page(connection.as_ref(), &position).await; + if rows.is_empty() { + break; + } + forward.extend(rows); + position = read.next.unwrap(); + } + // Going back ends on the first page, read again from the start. + let mut backward = Vec::new(); + let mut position = PagePosition::Last; + for _ in 0..declared.len() + 1 { + let (read, rows) = table.page(connection.as_ref(), &position).await; + if read.position == PagePosition::First { + let unseen: Vec = rows.into_iter().filter(|row| !backward.contains(row)).collect(); + backward.splice(0..0, unseen); + break; + } + backward.splice(0..0, rows); + position = read.previous.unwrap(); + } + + assert_eq!(forward, declared); + assert_eq!(backward, declared); + connection.close().await.unwrap(); +} + +/// A DATE key at its infinities pages like any other, the anchor past +/// `infinity` cast back to a DATE beside the key's other column. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_composite_key_with_an_infinite_date_pages_through() { + let file = TestFile::new(); + let connection = file.open().await; + apply( + connection.as_ref(), + &[ + "CREATE TABLE days (d DATE, n INTEGER, PRIMARY KEY (d, n))", + "INSERT INTO days VALUES ('infinity', 1), ('2024-01-01', 2), ('-infinity', 3), ('infinity', 0)", + ], + ) + .await + .unwrap(); + let table = Table::read(connection.as_ref(), "days").await; + let ordered: Vec = crate::support::query(connection.as_ref(), "SELECT d FROM days ORDER BY d, n") + .await + .rows + .into_iter() + .map(|row| row[0].clone()) + .collect(); + + let mut forward = Vec::new(); + let mut position = PagePosition::First; + for _ in 0..ordered.len() + 1 { + let (read, rows) = table.page(connection.as_ref(), &position).await; + if rows.is_empty() { + break; + } + forward.extend(rows); + position = read.next.unwrap(); + } + + assert_eq!(forward, ordered); + connection.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..7d27ab13b4 --- /dev/null +++ b/linux/crates/drivers/duckdb/tests/integration/sessions.rs @@ -0,0 +1,293 @@ +//! 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; + + // The read runs on a thread of its own and can end before the first + // poll returns, so each try gives one up on a new tab. + for _ in 0..50 { + 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); + if first.is_ready() { + editor.close().await; + continue; + } + let next = editor + .run( + tablepro_core::UserSql::new("SELECT 1"), + RunOptions::new(CancellationToken::new()), + crate::support::Collected::default().sink(), + ) + .await; + + assert_eq!(editor.state(), EditorSessionState::Lost); + assert!( + matches!(next, Err(tablepro_core::DriverError::ConnectionLost { .. })), + "{next:?}" + ); + editor.close().await; + connection.close().await.unwrap(); + return; + } + panic!("every state read finished before it was given up"); +} 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 ab52e50660..171cf3ff0d 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 01015381a1..08cad7f89b 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 507a7a4709..91aa856135 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/attach_guard.rs b/linux/crates/drivers/sqlite/src/attach_guard.rs new file mode 100644 index 0000000000..3b81b67ec6 --- /dev/null +++ b/linux/crates/drivers/sqlite/src/attach_guard.rs @@ -0,0 +1,145 @@ +//! Refusing an ATTACH of a file DuckDB has open in this process. +//! +//! SQLite would open the file, find no SQLite database in it and close it +//! again, and closing a descriptor drops every POSIX lock the process +//! holds on the file, DuckDB's among them. SQLite's authorizer is told +//! an ATTACH's file name only when the statement spells it as a string, +//! so one it computes is refused while DuckDB has any file open. The +//! editor runs an ATTACH under `tablepro_core::opening_file`, so DuckDB +//! cannot open the file between the check and SQLite's open. + +use std::ffi::{CStr, OsString, c_char, c_int, c_void}; +use std::os::unix::ffi::OsStringExt; +use std::path::PathBuf; + +use sqlx::sqlite::SqliteConnection as SqlxConnection; +use tablepro_core::DriverError; + +use crate::driver::ENGINE; +use crate::errors::map_error; + +/// Check every ATTACH the connection prepares from now on. +pub(crate) async fn install(connection: &mut SqlxConnection) -> Result<(), DriverError> { + let mut handle = connection.lock_handle().await.map_err(map_error)?; + let raw = handle.as_raw_handle(); + // SAFETY: `raw` comes from the locked handle, so the connection is + // open and nothing runs on it while the authorizer is set. The + // callback takes no user data. + let code = unsafe { libsqlite3_sys::sqlite3_set_authorizer(raw.as_ptr(), Some(authorize), std::ptr::null_mut()) }; + match code { + libsqlite3_sys::SQLITE_OK => Ok(()), + code => Err(DriverError::Protocol(format!( + "SQLite refused the ATTACH check with code {code}" + ))), + } +} + +/// SQLite's authorizer: every action passes but an ATTACH of a file +/// another engine has open. +unsafe extern "C" fn authorize( + _data: *mut c_void, + action: c_int, + file: *const c_char, + _second: *const c_char, + _database: *const c_char, + _trigger: *const c_char, +) -> c_int { + if action != libsqlite3_sys::SQLITE_ATTACH { + return libsqlite3_sys::SQLITE_OK; + } + if file.is_null() { + return match tablepro_core::any_claimed_by_another(ENGINE) { + true => libsqlite3_sys::SQLITE_DENY, + false => libsqlite3_sys::SQLITE_OK, + }; + } + // SAFETY: SQLite hands the file name as a NUL-terminated string that + // stays valid for the call. + let name = unsafe { CStr::from_ptr(file) }.to_bytes(); + let held = attached_paths(name) + .iter() + .any(|path| tablepro_core::claimed_by_another(path, ENGINE)); + match held { + true => libsqlite3_sys::SQLITE_DENY, + false => libsqlite3_sys::SQLITE_OK, + } +} + +/// The files an ATTACH may open: the name as a path, and for a `file:` +/// name also the URI's path, with its query left off and its escapes +/// decoded as SQLite decodes them, since whether SQLite reads the name as +/// a URI depends on how the connection was opened. Relative to the +/// working directory, as SQLite opens them. +fn attached_paths(name: &[u8]) -> Vec { + if name.is_empty() || name == b":memory:" { + return Vec::new(); + } + let mut paths = vec![PathBuf::from(OsString::from_vec(name.to_vec()))]; + if let Some(uri) = name.strip_prefix(b"file:") { + let end = uri + .iter() + .position(|byte| matches!(byte, b'?' | b'#')) + .unwrap_or(uri.len()); + let path = uri.get(..end).unwrap_or_default(); + let path = path + .strip_prefix(b"//localhost") + .or_else(|| path.strip_prefix(b"//")) + .unwrap_or(path); + let decoded = percent_decoded(path); + if !decoded.is_empty() && decoded != b":memory:" { + paths.push(PathBuf::from(OsString::from_vec(decoded))); + } + } + paths +} + +/// The path with each `%XX` escape decoded, as SQLite decodes a URI: a +/// `%` that two hex digits do not follow stays as it is, and an escaped +/// NUL ends the path. +fn percent_decoded(text: &[u8]) -> Vec { + let hex = |byte: Option<&u8>| byte.and_then(|byte| char::from(*byte).to_digit(16)); + let mut decoded = Vec::with_capacity(text.len()); + let mut index = 0; + while let Some(&byte) = text.get(index) { + let escaped = match byte { + b'%' => hex(text.get(index + 1)).zip(hex(text.get(index + 2))), + _ => None, + }; + let (byte, width) = match escaped { + Some((high, low)) => (u8::try_from(high * 16 + low).unwrap_or(0), 3), + None => (byte, 1), + }; + if byte == 0 { + break; + } + decoded.push(byte); + index += width; + } + decoded +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_attach_names_a_path_and_a_uris_path() { + let paths = |name: &str| attached_paths(name.as_bytes()); + let path = |text: &str| PathBuf::from(text); + + assert_eq!(paths("/srv/a.db"), [path("/srv/a.db")]); + assert_eq!(paths("rel.db"), [path("rel.db")]); + assert_eq!( + paths("file:/srv/a%20b.db?mode=ro"), + [path("file:/srv/a%20b.db?mode=ro"), path("/srv/a b.db")] + ); + assert_eq!(paths("file:///srv/a.db")[1], path("/srv/a.db")); + assert_eq!(paths("file://localhost/srv/a.db")[1], path("/srv/a.db")); + assert_eq!(paths("file:/srv/data.duckdb%00ignored")[1], path("/srv/data.duckdb")); + assert_eq!(paths("file:/srv/held%GG.duckdb")[1], path("/srv/held%GG.duckdb")); + assert_eq!(paths("file:/srv/end%2")[1], path("/srv/end%2")); + assert!(paths(":memory:").is_empty()); + assert_eq!(paths("file::memory:?cache=shared").len(), 1); + assert!(paths("").is_empty()); + } +} diff --git a/linux/crates/drivers/sqlite/src/capabilities.rs b/linux/crates/drivers/sqlite/src/capabilities.rs index 3c18bb16c8..8c2497ab1c 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, - StatementStop, WriteAtomicity, + CommitResolutionSupport, DriverCapabilities, EndpointKind, EstimateQuality, FileFormat, FileSignature, + ReadOnlyEnforcement, ScriptExecution, StatementStop, 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 @@ -10,7 +23,7 @@ use tablepro_core::{ /// so there is nothing to resolve afterwards. pub(crate) static CAPABILITIES: DriverCapabilities = DriverCapabilities { data_model: tablepro_core::DataModel::Relational, - endpoint: EndpointKind::File, + endpoint: EndpointKind::File(FILE_FORMAT), auth_modes: &[], username_required: false, tls: None, diff --git a/linux/crates/drivers/sqlite/src/connection.rs b/linux/crates/drivers/sqlite/src/connection.rs index f29248665e..941bf5ce47 100644 --- a/linux/crates/drivers/sqlite/src/connection.rs +++ b/linux/crates/drivers/sqlite/src/connection.rs @@ -61,19 +61,40 @@ impl SqliteConnection { liveness: LivenessPolicy, ) -> Result { let path = endpoint.path().to_path_buf(); + // Until the pool holds a descriptor on the file, a header read + // elsewhere in the process could close one and drop its locks. + let opening = tablepro_core::opening_file().await; + // SQLite would close a DuckDB file again on finding no database in + // it, which drops the locks DuckDB holds on it. + if tablepro_core::claimed_by_another(&path, crate::driver::ENGINE) { + return Err(DriverError::Busy(tablepro_core::BusyReason::FileLocked)); + } if endpoint.mode() == FileOpenMode::CreateNew { let target = path.clone(); tokio::task::spawn_blocking(move || create_or_replace(&target)) .await .map_err(|error| DriverError::Protocol(format!("creating the database file stopped: {error}")))??; } + // A FIFO would hold the open, and every other engine's open behind + // it, until something wrote to it. + if std::fs::metadata(&path).is_ok_and(|metadata| !metadata.is_file()) { + return Err(DriverError::NotADatabase { path }); + } let options = connect_options(&path, settings.read_only, settings.lock_timeout); let pool = SqlitePoolOptions::new() .max_connections(u32::try_from(WORKSPACE_POOL_MAX_SIZE).unwrap_or(1)) .acquire_timeout(liveness.pool_wait) + .after_connect(|connection, _| { + Box::pin(async move { + crate::attach_guard::install(connection) + .await + .map_err(|error| sqlx::Error::Protocol(error.to_string())) + }) + }) .connect_with(options) .await .map_err(|error| map_open_error(error, &path))?; + drop(opening); let version = match check_file(&pool, &path).await { Ok(version) => version, @@ -205,7 +226,11 @@ impl SqliteConnection { phase: TimeoutPhase::Statement, server_cancelled: false, }), - opened = SqlxConnection::connect_with(&read_only) => opened.map_err(|error| map_open_error(error, &self.path)), + opened = SqlxConnection::connect_with(&read_only) => { + let mut connection = opened.map_err(|error| map_open_error(error, &self.path))?; + crate::attach_guard::install(&mut connection).await?; + Ok(connection) + } } } } diff --git a/linux/crates/drivers/sqlite/src/create_file.rs b/linux/crates/drivers/sqlite/src/create_file.rs index 84c2dbd592..a5611509eb 100644 --- a/linux/crates/drivers/sqlite/src/create_file.rs +++ b/linux/crates/drivers/sqlite/src/create_file.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; -use tablepro_core::DriverError; +use tablepro_core::{BusyReason, DriverError}; /// The journal and WAL files SQLite keeps beside a database. One left /// from the database being replaced would be replayed into the new one. @@ -29,6 +29,11 @@ pub(crate) fn create_or_replace(path: &Path) -> Result<(), DriverError> { path: path.to_path_buf(), }); }; + // A file another window or the other engine has open would be pulled + // out from under it, and its writes would go to a file no name finds. + if tablepro_core::held_by_this_process(path) { + return Err(DriverError::Busy(BusyReason::FileLocked)); + } let temporary = parent.join(format!( ".{}.{}.tmp", name.to_string_lossy(), diff --git a/linux/crates/drivers/sqlite/src/ddl/mod.rs b/linux/crates/drivers/sqlite/src/ddl/mod.rs index 3008d32423..14ca4a6451 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/drivers/sqlite/src/driver.rs b/linux/crates/drivers/sqlite/src/driver.rs index 4d426f243d..5c5ca35d92 100644 --- a/linux/crates/drivers/sqlite/src/driver.rs +++ b/linux/crates/drivers/sqlite/src/driver.rs @@ -15,6 +15,10 @@ use crate::connection::SqliteConnection; /// attached database stay visible from one call to the next. pub const WORKSPACE_POOL_MAX_SIZE: usize = 1; +/// The driver id, which also names the engine in the process's file +/// claims (`tablepro_core::claim_file`). +pub(crate) const ENGINE: &str = "sqlite"; + /// The SQLite engine. #[derive(Debug, Clone, Copy, Default)] pub struct SqliteDriver; @@ -22,7 +26,7 @@ pub struct SqliteDriver; #[async_trait] impl DatabaseDriver for SqliteDriver { fn id(&self) -> &'static str { - "sqlite" + ENGINE } fn display_name(&self) -> &'static str { diff --git a/linux/crates/drivers/sqlite/src/editor_session.rs b/linux/crates/drivers/sqlite/src/editor_session.rs index 8d40cde552..e5886f7d5d 100644 --- a/linux/crates/drivers/sqlite/src/editor_session.rs +++ b/linux/crates/drivers/sqlite/src/editor_session.rs @@ -64,12 +64,18 @@ impl SqliteEditorSession { lock_timeout: Duration, cancel: CancellationToken, ) -> Result { - let connecting = SqlxConnection::connect_with(options); + let connecting = async { + let mut connection = SqlxConnection::connect_with(options) + .await + .map_err(|error| map_open_error(error, path))?; + crate::attach_guard::install(&mut connection).await?; + Ok::<_, DriverError>(connection) + }; let connection = tokio::select! { biased; () = cancel.cancelled() => return Err(DriverError::Cancelled), opened = tokio::time::timeout(liveness.connect_timeout, connecting) => match opened { - Ok(opened) => opened.map_err(|error| map_open_error(error, path))?, + Ok(opened) => opened?, Err(_) => { return Err(DriverError::Timeout { phase: TimeoutPhase::Connect, diff --git a/linux/crates/drivers/sqlite/src/lib.rs b/linux/crates/drivers/sqlite/src/lib.rs index be5691f874..5d4eaf16d2 100644 --- a/linux/crates/drivers/sqlite/src/lib.rs +++ b/linux/crates/drivers/sqlite/src/lib.rs @@ -1,6 +1,7 @@ //! The SQLite driver: one database file, one workspace connection, and //! an editor session per tab on a connection of its own. +mod attach_guard; mod autocommit; mod capabilities; mod catalog; diff --git a/linux/crates/drivers/sqlite/src/result_stream.rs b/linux/crates/drivers/sqlite/src/result_stream.rs index 05e0db441f..609c89b9a4 100644 --- a/linux/crates/drivers/sqlite/src/result_stream.rs +++ b/linux/crates/drivers/sqlite/src/result_stream.rs @@ -90,6 +90,17 @@ pub(crate) async fn run_script( ) -> Result { let mut outcome = StreamOutcome::default(); for statement in split_statements(sql) { + if halt() { + outcome.halted = true; + break; + } + // An ATTACH opens a file, so it takes its turn with DuckDB's opens, + // and its check in `attach_guard` holds until it is done. + let _opening = match first_word(statement).is_some_and(|word| word.eq_ignore_ascii_case("ATTACH")) { + true => Some(tablepro_core::opening_file().await), + false => None, + }; + // A stop that came while the ATTACH waited its turn. if halt() { outcome.halted = true; break; @@ -165,21 +176,24 @@ pub(crate) fn split_statements(sql: &str) -> Vec<&str> { /// only INSERT, UPDATE and DELETE, and after anything else still holds /// the count of the last one of those. pub(crate) fn reports_changes(sql: &str) -> bool { + first_word(sql).is_some_and(|word| { + ["INSERT", "UPDATE", "DELETE", "REPLACE", "WITH"] + .iter() + .any(|keyword| word.eq_ignore_ascii_case(keyword)) + }) +} + +/// The statement's first word, past comments. +fn first_word(sql: &str) -> Option<&str> { let plan = ScriptPlan::build( sql, SqlGrammar::Sqlite, LexicalSettings::default_for(SqlGrammar::Sqlite), ); - let first = plan - .tokens(0..sql.len()) + plan.tokens(0..sql.len()) .find(|token| !token.kind.is_trivia()) .filter(|token| token.kind == ScriptTokenKind::Word) - .and_then(|token| sql.get(token.range)); - first.is_some_and(|word| { - ["INSERT", "UPDATE", "DELETE", "REPLACE", "WITH"] - .iter() - .any(|keyword| word.eq_ignore_ascii_case(keyword)) - }) + .and_then(|token| sql.get(token.range)) } async fn run_statement( diff --git a/linux/crates/drivers/sqlite/tests/integration/open_semantics.rs b/linux/crates/drivers/sqlite/tests/integration/open_semantics.rs index 5a61080652..e5cb02bf1a 100644 --- a/linux/crates/drivers/sqlite/tests/integration/open_semantics.rs +++ b/linux/crates/drivers/sqlite/tests/integration/open_semantics.rs @@ -196,3 +196,87 @@ async fn the_server_info_names_the_library() { assert!(info.at_least(3, 37, 0), "{info:?}"); connection.close().await.unwrap(); } + +/// SQLite would close a DuckDB file again on finding no database in it, +/// which drops the locks DuckDB holds on it, so neither a connect nor a +/// tab's ATTACH opens a file DuckDB has open. +#[tokio::test] +async fn a_file_duckdb_has_open_is_not_opened() { + let file = TestFile::new().await; + let held = file.directory().join("held.duckdb"); + std::fs::write(&held, b"a DuckDB database").unwrap(); + // DuckDB claims a file and keeps a descriptor on it while it is open. + let _claim = tablepro_core::claim_file(&held, "duckdb").unwrap(); + let _descriptor = std::fs::File::open(&held).unwrap(); + let connection = file.open().await; + let editor = connection + .open_editor_session(tablepro_core::EditorSessionOptions::default(), CancellationToken::new()) + .await + .unwrap(); + + let opened = connect(FileEndpoint::open_existing(held.clone()).unwrap(), writable()).await; + let (attached, _) = crate::support::run(editor.as_ref(), &format!("ATTACH '{}' AS held", held.display())).await; + let (attached_uri, _) = crate::support::run( + editor.as_ref(), + &format!("ATTACH 'file:{}?mode=ro' AS held", held.display()), + ) + .await; + let (attached_computed, _) = + crate::support::run(editor.as_ref(), &format!("ATTACH ('{}' || '') AS held", held.display())).await; + + assert_eq!( + opened.err(), + Some(DriverError::Busy(tablepro_core::BusyReason::FileLocked)) + ); + for end in [attached.end, attached_uri.end, attached_computed.end] { + assert!(matches!(end, tablepro_core::RunEnd::Failed(_)), "{end:?}"); + } + let other_path = file.directory().join("other.sqlite3"); + create_database(&other_path).await.unwrap(); + let (other, _) = crate::support::run(editor.as_ref(), &format!("ATTACH '{}' AS other", other_path.display())).await; + assert_eq!(other.end, tablepro_core::RunEnd::Completed); + editor.close().await; + connection.close().await.unwrap(); +} + +/// A new database over a file this process has open would pull it out +/// from under the window using it. +#[tokio::test] +async fn create_new_refuses_a_file_this_process_holds() { + let file = TestFile::new().await; + let open = file.open().await; + apply(open.as_ref(), &["CREATE TABLE kept (a)"]).await.unwrap(); + + let created = connect(FileEndpoint::create_new(file.path.clone()).unwrap(), writable()).await; + + assert_eq!( + created.err(), + Some(DriverError::Busy(tablepro_core::BusyReason::FileLocked)) + ); + let tables = query(open.as_ref(), "SELECT count(*) FROM sqlite_schema").await; + assert_eq!(tables.rows[0][0], Value::Int(1)); + open.close().await.unwrap(); +} + +/// Opening a FIFO read-only would wait for a writer, with every other +/// open in the process waiting behind it. +#[tokio::test] +async fn a_fifo_is_no_database() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("pipe.sqlite3"); + assert!( + std::process::Command::new("mkfifo") + .arg(&path) + .status() + .unwrap() + .success() + ); + + let opened = connect( + FileEndpoint::open_existing(path.clone()).unwrap(), + WorkspaceSettings::workspace(true), + ) + .await; + + assert_eq!(opened.err(), Some(DriverError::NotADatabase { path })); +} diff --git a/linux/crates/test-support/src/conformance/cases/ddl.rs b/linux/crates/test-support/src/conformance/cases/ddl.rs index e006f1bd22..1de1b1fbf3 100644 --- a/linux/crates/test-support/src/conformance/cases/ddl.rs +++ b/linux/crates/test-support/src/conformance/cases/ddl.rs @@ -176,7 +176,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, @@ -250,12 +250,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..) @@ -480,13 +478,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| { @@ -526,13 +520,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(()) } @@ -556,13 +546,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(()); @@ -587,13 +575,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 17924d21a2..99e9fd77db 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 0df01968f9..f798a25dff 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 7bfdd844ab..77a7be580b 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, DataModel, DatabaseDriver, DriverCapabilities, - DriverError, EndpointKind, EstimateQuality, ReadOnlyEnforcement, ScriptExecution, ServerNameOverride, - StatementStop, TimeoutPhase, TlsCapabilities, TlsMode, WriteAtomicity, + DriverError, EndpointKind, EstimateQuality, FileFormat, FileSignature, ReadOnlyEnforcement, ScriptExecution, + ServerNameOverride, StatementStop, 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>>, @@ -58,13 +59,40 @@ impl FakeDriver { ..Self::NETWORK_CAPABILITIES }; + /// 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>, @@ -86,6 +114,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()), @@ -93,6 +122,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() } @@ -105,7 +140,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 22941f81e8..ff9026ffed 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, ClickHouse, Cassandra and ScyllaDB databases -Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;SQL Server;ClickHouse;Cassandra;ScyllaDB;CQL; +Comment=Browse and edit PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, Cassandra and ScyllaDB databases +Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;DuckDB;SQL Server;ClickHouse;Cassandra;ScyllaDB;CQL; 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 923288ae5d..b4342a90bc 100644 --- a/linux/data/app.tablepro.TablePro.metainfo.xml.in.in +++ b/linux/data/app.tablepro.TablePro.metainfo.xml.in.in @@ -8,9 +8,10 @@

Work with your databases

- TablePro connects to PostgreSQL, MySQL, SQLite, SQL Server, ClickHouse, - Cassandra and ScyllaDB. 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, + ClickHouse, Cassandra and ScyllaDB. 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/NOTICE b/linux/data/licenses/NOTICE new file mode 100644 index 0000000000..50a98d7515 --- /dev/null +++ b/linux/data/licenses/NOTICE @@ -0,0 +1,21 @@ +The NOTICE files of the crates linked into TablePro, as the Apache License +2.0 asks. build-aux/update-third-party-licenses.sh writes this file. + +Used by: arrow 58.4.0, 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 + +Apache Arrow +Copyright 2016-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes software from the chronoutil crate (MIT) + * Copyright (c) 2020-2022 Oliver Margetts + * https://github.com/olliemath/chronoutil + +This product includes software from the compact-thrift project (Apache 2.0) + * Copyright Jörn Horstmann + * https://github.com/jhorstmann/compact-thrift diff --git a/linux/data/licenses/THIRD-PARTY-NOTICE.md b/linux/data/licenses/THIRD-PARTY-NOTICE.md index 69928ca764..73803d8d89 100644 --- a/linux/data/licenses/THIRD-PARTY-NOTICE.md +++ b/linux/data/licenses/THIRD-PARTY-NOTICE.md @@ -21,6 +21,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 @@ -3568,6 +3579,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: + - rand_pcg 0.9.0 ~~~~text @@ -3743,6 +3789,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 @@ -3778,6 +3826,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 @@ -3946,6 +3995,7 @@ DEALINGS IN THE SOFTWARE. Used by: - hashbrown 0.14.5 +- hashbrown 0.15.5 - hashbrown 0.16.1 - hashbrown 0.17.1 @@ -4086,6 +4136,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 @@ -4816,6 +4895,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 @@ -5931,7 +6045,7 @@ DEALINGS IN THE SOFTWARE. Used by: -- crypto-bigint 0.7.5 +- crypto-bigint 0.7.6 - spki 0.8.0 ~~~~text @@ -6205,7 +6319,7 @@ SOFTWARE. Used by: -- ctutils 0.4.4 +- ctutils 0.4.5 ~~~~text Copyright (c) 2025-2026 The RustCrypto Project Developers @@ -6517,6 +6631,24 @@ SOFTWARE. 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 @@ -6890,6 +7022,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: + - schemars 1.2.2 - schemars_derive 1.2.2 @@ -6953,6 +7116,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 @@ -7047,6 +7242,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 @@ -7357,6 +7583,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 @@ -7405,7 +7662,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 @@ -7418,6 +7682,7 @@ Used by: - ref-cast 1.0.27 - 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 @@ -8301,6 +8566,7 @@ SOFTWARE. Used by: +- hashlink 0.10.0 - hashlink 0.11.1 ~~~~text @@ -8760,6 +9026,7 @@ freely, subject to the following restrictions: Used by: +- foldhash 0.1.5 - foldhash 0.2.0 ~~~~text @@ -8783,3 +9050,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 15df188d6c..9b4296f084 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" } } ] @@ -313,6 +324,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", @@ -362,7 +381,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" } } ] }, { @@ -370,7 +391,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" } } ] }, { @@ -419,6 +441,7 @@ "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.14.5" } }, + { "crate": { "name": "hashbrown", "version": "0.15.5" } }, { "crate": { "name": "hashbrown", "version": "0.16.1" } }, { "crate": { "name": "hashbrown", "version": "0.17.1" } } ] @@ -452,6 +475,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", @@ -624,6 +655,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", @@ -899,7 +938,7 @@ "name": "MIT License", "text": "Copyright (c) 2021-2026 The RustCrypto 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.\n", "used_by": [ - { "crate": { "name": "crypto-bigint", "version": "0.7.5" } }, + { "crate": { "name": "crypto-bigint", "version": "0.7.6" } }, { "crate": { "name": "spki", "version": "0.8.0" } } ] }, @@ -969,7 +1008,7 @@ "name": "MIT License", "text": "Copyright (c) 2025-2026 The RustCrypto 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.\n", "used_by": [ - { "crate": { "name": "ctutils", "version": "0.4.4" } } + { "crate": { "name": "ctutils", "version": "0.4.5" } } ] }, { @@ -1053,6 +1092,15 @@ { "crate": { "name": "scylla", "version": "1.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", @@ -1154,6 +1202,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", @@ -1171,6 +1227,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", @@ -1195,6 +1260,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", @@ -1280,6 +1353,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", @@ -1309,7 +1390,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" } }, @@ -1322,6 +1410,7 @@ { "crate": { "name": "ref-cast", "version": "1.0.27" } }, { "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" } }, @@ -1596,6 +1685,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" } } ] }, @@ -1658,16 +1748,272 @@ "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": "arc-swap", "version": "1.9.3", "license": "MIT OR Apache-2.0" }, { "name": "arcstr", "version": "1.2.0", "license": "Apache-2.0 OR MIT OR Zlib" }, + { "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-lock", "version": "3.4.2", "license": "Apache-2.0 OR MIT" }, @@ -1692,6 +2038,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" }, @@ -1702,6 +2049,7 @@ { "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": "combine", "version": "4.6.8", "license": "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" }, @@ -1714,13 +2062,14 @@ { "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": "crypto-bigint", "version": "0.7.5", "license": "Apache-2.0 OR MIT" }, + { "name": "crossterm", "version": "0.28.1", "license": "MIT" }, + { "name": "crypto-bigint", "version": "0.7.6", "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" }, { "name": "crypto-primes", "version": "0.7.2", "license": "Apache-2.0 OR MIT" }, { "name": "csv", "version": "1.4.0", "license": "Unlicense OR MIT" }, { "name": "csv-core", "version": "0.1.13", "license": "Unlicense OR MIT" }, - { "name": "ctutils", "version": "0.4.4", "license": "Apache-2.0 OR MIT" }, + { "name": "ctutils", "version": "0.4.5", "license": "Apache-2.0 OR MIT" }, { "name": "darling", "version": "0.24.1", "license": "MIT" }, { "name": "darling_core", "version": "0.24.1", "license": "MIT" }, { "name": "darling_macro", "version": "0.24.1", "license": "MIT" }, @@ -1735,6 +2084,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": "dyn-clone", "version": "1.0.20", "license": "MIT OR Apache-2.0" }, { "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" }, @@ -1746,12 +2096,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" }, @@ -1793,8 +2146,10 @@ { "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.14.5", "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" }, @@ -1830,13 +2185,21 @@ { "name": "itertools", "version": "0.15.0", "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" }, @@ -1904,6 +2267,7 @@ { "name": "rmcp", "version": "3.6.0", "license": "Apache-2.0 AND 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" }, @@ -1958,6 +2322,8 @@ { "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": "strsim", "version": "0.11.1", "license": "MIT" }, + { "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" }, @@ -1995,6 +2361,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/meson.build b/linux/data/meson.build index 3943a2b943..fa07f7bff1 100644 --- a/linux/data/meson.build +++ b/linux/data/meson.build @@ -88,6 +88,7 @@ install_man('tablepro.1') if get_option('flatpak') install_data( 'licenses' / 'THIRD-PARTY-NOTICE.md', + 'licenses' / 'NOTICE', install_dir: get_option('datadir') / 'licenses' / application_id, ) endif diff --git a/linux/data/resources/resources.gresource.xml b/linux/data/resources/resources.gresource.xml index 6f2c97dfad..0f5c67884d 100644 --- a/linux/data/resources/resources.gresource.xml +++ b/linux/data/resources/resources.gresource.xml @@ -32,7 +32,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 1bc64701fa..bcce86f17f 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 94% rename from linux/data/resources/ui/sqlite-file-row.ui rename to linux/data/resources/ui/database-file-row.ui index d5f296ee88..5075062965 100644 --- a/linux/data/resources/ui/sqlite-file-row.ui +++ b/linux/data/resources/ui/database-file-row.ui @@ -2,7 +2,7 @@ -