diff --git a/.cargo/config.toml b/.cargo/config.toml index d22ed76a..9247b719 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,3 +5,6 @@ # set by the user, and this is a good thing. If the user already set some # LIBSQLITE3_FLAGS, he probably knows what he is doing. LIBSQLITE3_FLAGS = "-DSQLITE_ENABLE_MATH_FUNCTIONS" + +[build] +rustflags = [] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bf28e9a..87bae98b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,15 +27,11 @@ jobs: - uses: actions/checkout@v4 - run: npm ci - run: npm test - - name: Install ODBC dependencies - run: | - sudo apt-get update - sudo apt-get install -y unixodbc-dev freetds-dev - name: Set up cargo cache uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - run: cargo fmt --all -- --check - run: cargo clippy --all-targets --all-features -- -D warnings - - run: cargo test + - run: cargo test --all-features - name: Upload Linux binary uses: actions/upload-artifact@v4 with: @@ -58,21 +54,15 @@ jobs: db_url: "mssql://root:Password123!@127.0.0.1/sqlpage" - database: odbc container: postgres - db_url: "Driver={PostgreSQL Unicode};Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!" + db_url: "Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!" setup_odbc: true steps: - uses: actions/checkout@v4 - name: Set up cargo cache uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - - name: Install ODBC dependencies - run: | - sudo apt-get update - sudo apt-get install -y unixodbc-dev freetds-dev - - name: Setup ODBC for testing + - name: Install PostgreSQL ODBC driver if: matrix.setup_odbc - run: | - # Install PostgreSQL ODBC driver (automatically registers the driver) - sudo apt-get install -y odbc-postgresql + run: sudo apt-get install -y odbc-postgresql - name: Start database container run: docker compose up --wait ${{ matrix.container }} - name: Show container logs @@ -80,7 +70,7 @@ jobs: run: docker compose logs ${{ matrix.container }} - name: Run tests against ${{ matrix.database }} timeout-minutes: 5 - run: cargo test + run: cargo test --all-features env: DATABASE_URL: ${{ matrix.db_url }} RUST_BACKTRACE: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97dcf3ec..e67936fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,8 +24,10 @@ jobs: - os: windows-latest binary_extension: .exe target: x86_64-pc-windows-msvc + features: "" - os: macos-latest target: x86_64-apple-darwin + features: "odbc-static" steps: - uses: actions/checkout@v4 - name: Install Rust toolchain @@ -35,7 +37,7 @@ jobs: - name: Set up cargo cache uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - name: Build - run: cargo build --profile superoptimized --locked --target ${{ matrix.target }} + run: cargo build --profile superoptimized --locked --target ${{ matrix.target }} --features "${{ matrix.features }}" - name: Upload unsigned Windows artifact if: matrix.os == 'windows-latest' id: upload_unsigned @@ -83,10 +85,6 @@ jobs: container: quay.io/pypa/manylinux_2_28_x86_64 steps: - uses: actions/checkout@v4 - - name: Install ODBC dependencies - run: | - yum update -y - yum install -y unixODBC-devel freetds-devel - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: @@ -94,7 +92,7 @@ jobs: - name: Set up cargo cache uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - name: Build - run: cargo build --profile superoptimized --locked --target x86_64-unknown-linux-gnu + run: cargo build --profile superoptimized --locked --target x86_64-unknown-linux-gnu --features "odbc-static" - uses: actions/upload-artifact@v4 with: name: sqlpage ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 1d9f5acd..b7c66435 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,16 +3,17 @@ HTML streamed to client ## Validation +### When working on rust code Mandatory formatting (rust): `cargo fmt --all` - Mandatory linting: `cargo clippy --all-targets --all-features -- -D warnings` +### When working on css or js Frontend formatting: `npm run format` More about testing: see [github actions](./.github/workflows/ci.yml). Project structure: see [contribution guide](./CONTRIBUTING.md) -Don’t reformat unrelated files. Always run tests/lints/format before stopping when you changed code. +NEVER reformat/lint/touch files unrelated to your task. Always run tests/lints/format before stopping when you changed code. ### Testing @@ -32,33 +33,10 @@ DATABASE_URL='mssql://root:Password123!@localhost/sqlpage' cargo test # all dbms - Components: defined in `./sqlpage/templates/*.handlebars` - Functions: `src/webserver/database/sqlpage_functions/functions.rs` registered with `make_function!`. - Components and functions are documented in [official website](./examples/official-site/sqlpage/migrations/); one migration per component and per function. -- ```sql - CREATE TABLE component( - name TEXT PRIMARY KEY, - description TEXT NOT NULL, - icon TEXT, -- icon name from tabler icon - introduced_in_version TEXT - ); - - CREATE TABLE parameter_type(name TEXT PRIMARY KEY); - INSERT INTO parameter_type(name) VALUES ('BOOLEAN'), ('COLOR'), ('HTML'), ('ICON'), ('INTEGER'), ('JSON'), ('REAL'), ('TEXT'), ('TIMESTAMP'), ('URL'); - - CREATE TABLE parameter( - top_level BOOLEAN DEFAULT FALSE, - name TEXT, - component TEXT REFERENCES component(name) ON DELETE CASCADE, - description TEXT, - description_md TEXT, - type TEXT REFERENCES parameter_type(name) ON DELETE CASCADE, - optional BOOLEAN DEFAULT FALSE, - ); - - CREATE TABLE example( - component TEXT REFERENCES component(name) ON DELETE CASCADE, - description TEXT, - properties JSON, - ); - ``` + - tables + - `component(name,description,icon,introduced_in_version)` -- icon name from tabler icon + - `parameter(top_level BOOLEAN, name, component REFERENCES component(name), description, description_md, type, optional BOOLEAN)` parameter types: BOOLEAN, COLOR, HTML, ICON, INTEGER, JSON, REAL, TEXT, TIMESTAMP, URL + - `example(component REFERENCES component(name), description, properties JSON)` - [Configuration](./configuration.md): see [AppConfig](./src/app_config.rs) - Routing: file-based in `src/webserver/routing.rs`; not found handled via `src/default_404.sql`. - Follow patterns from similar modules before introducing new abstractions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3735646f..e6ad47db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,12 +37,24 @@ cargo build --release The resulting executable will be in `target/release/sqlpage`. +### ODBC build modes + +SQLPage can either be built with an integrated odbc driver manager (static linking), +or depend on having one already installed on the system where it is running (dynamic linking). + +- Dynamic ODBC (default): `cargo build` +- Static ODBC (Linux and MacOS only): `cargo build --features odbc-static` + +Windows comes with ODBC pre-installed; SQLPage cannot statically link to the unixODBC driver manager on windows. + ## Code Style and Linting ### Rust + - Use `cargo fmt` to format your Rust code - Run `cargo clippy` to catch common mistakes and improve code quality - All code must pass the following checks: + ```bash cargo fmt --all -- --check cargo clippy @@ -55,6 +67,7 @@ We use Biome for linting and formatting of the frontend code. ```bash npx @biomejs/biome check . ``` + This will check the entire codebase (html, css, js). ## Testing @@ -80,6 +93,7 @@ cargo test ``` ### End-to-End Tests + We use Playwright for end-to-end testing of dynamic frontend features. Tests are located in [`tests/end-to-end/`](./tests/end-to-end/). Key areas covered include: @@ -103,6 +117,7 @@ npm run test ## Documentation ### Component Documentation + When adding new components, comprehensive documentation is required. Example from a component documentation: ```sql @@ -110,7 +125,7 @@ INSERT INTO component(name, icon, description, introduced_in_version) VALUES ('component_name', 'icon_name', 'Description of the component', 'version'); -- Document all parameters -INSERT INTO parameter(component, name, description, type, top_level, optional) +INSERT INTO parameter(component, name, description, type, top_level, optional) VALUES ('component_name', 'param_name', 'param_description', 'TEXT|BOOLEAN|NUMBER|JSON|ICON|COLOR', false, true); -- Include usage examples @@ -127,6 +142,7 @@ If you are editing an existing component, edit the existing sql documentation fi If you are adding a new component, add a new sql file in the folder, and add the appropriate insert statements above. ### SQLPage Function Documentation + When adding new SQLPage functions, document them using a SQL migrations. Example structure: ```sql @@ -168,6 +184,7 @@ VALUES ( ``` Key elements to include in function documentation: + - Clear description of the function's purpose - Version number where the function was introduced - Appropriate icon @@ -179,11 +196,13 @@ Key elements to include in function documentation: ## Pull Request Process 1. Create a new branch for your feature/fix: + ```bash git checkout -b feature/your-feature-name ``` 2. Make your changes, ensuring: + - All tests pass - Code is properly formatted - New features are documented @@ -199,11 +218,10 @@ git checkout -b feature/your-feature-name - Run frontend linting with Biome - Test against multiple databases (PostgreSQL, MySQL, MSSQL) -5. Wait for review and address any feedback - ## Release Process Releases are automated when pushing tags that match the pattern `v*` (e.g., `v1.0.0`). The CI pipeline will: + - Build and test the code - Create Docker images for multiple architectures - Push images to Docker Hub diff --git a/Cargo.lock b/Cargo.lock index a2b58f12..dd43bdd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -895,9 +895,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.39" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "jobserver", @@ -1739,9 +1739,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "flate2" @@ -3123,8 +3123,10 @@ dependencies = [ [[package]] name = "odbc-sys" version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb069b57ebbad5234fb7197af7ee0c40daceb3946a86fa8d3f7a38393bf2770" +source = "git+https://github.com/sqlpage/odbc-sys?branch=main#0b5489e7b07d45e6b126f5a57ed6c301f022d2da" +dependencies = [ + "cc", +] [[package]] name = "oid-registry" @@ -3327,20 +3329,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.17", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -3348,9 +3349,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -3361,9 +3362,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -4129,9 +4130,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64 0.22.1", "chrono", @@ -4140,8 +4141,7 @@ dependencies = [ "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -4149,9 +4149,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -4298,6 +4298,7 @@ dependencies = [ "log", "markdown", "mime_guess", + "odbc-sys", "openidconnect", "password-hash", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index 27966e64..ab4c2931 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,16 @@ clap = { version = "4.5.17", features = ["derive"] } tokio-util = "0.7.12" openidconnect = { version = "4.0.0", default-features = false } encoding_rs = "0.8.35" +odbc-sys = { version = "0.27", features = [], optional = false } + + +[features] +default = [] +odbc-static = ["odbc-sys/static"] + + +[patch.crates-io] +odbc-sys = { git = "https://github.com/sqlpage/odbc-sys", branch = "main" } # see https://github.com/pacman82/odbc-sys/pull/60 [build-dependencies] awc = { version = "3", features = ["rustls-0_23-webpki-roots"] } diff --git a/Dockerfile b/Dockerfile index 8e930dec..5d83e202 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,37 +7,43 @@ RUN apt-get update && \ if [ "$TARGETARCH" = "$BUILDARCH" ]; then \ rustup target list --installed > TARGET && \ echo gcc > LINKER && \ - apt-get install -y gcc libgcc-s1 cmake unixodbc-dev libodbc2 libltdl7 && \ - LIBDIR="/lib/*"; \ - USRLIBDIR="/usr/lib/*"; \ + apt-get install -y gcc libgcc-s1 cmake unixodbc-dev libltdl-dev pkg-config && \ + LIBMULTIARCH=$(gcc -print-multiarch); \ + LIBDIR="/lib/$LIBMULTIARCH"; \ + USRLIBDIR="/usr/lib/$LIBMULTIARCH"; \ + HOST_TRIPLE=$(gcc -dumpmachine); \ elif [ "$TARGETARCH" = "arm64" ]; then \ echo aarch64-unknown-linux-gnu > TARGET && \ echo aarch64-linux-gnu-gcc > LINKER && \ dpkg --add-architecture arm64 && apt-get update && \ - apt-get install -y gcc-aarch64-linux-gnu libgcc-s1-arm64-cross unixodbc-dev:arm64 libodbc2:arm64 libltdl7:arm64 && \ + apt-get install -y gcc-aarch64-linux-gnu libgcc-s1-arm64-cross unixodbc-dev:arm64 libltdl-dev:arm64 pkg-config && \ LIBDIR="/lib/aarch64-linux-gnu"; \ USRLIBDIR="/usr/lib/aarch64-linux-gnu"; \ + HOST_TRIPLE="aarch64-linux-gnu"; \ elif [ "$TARGETARCH" = "arm" ]; then \ echo armv7-unknown-linux-gnueabihf > TARGET && \ echo arm-linux-gnueabihf-gcc > LINKER && \ dpkg --add-architecture armhf && apt-get update && \ - apt-get install -y gcc-arm-linux-gnueabihf libgcc-s1-armhf-cross cmake libclang1 clang unixodbc-dev:armhf libodbc2:armhf libltdl7:armhf && \ + apt-get install -y gcc-arm-linux-gnueabihf libgcc-s1-armhf-cross cmake libclang1 clang unixodbc-dev:armhf libltdl-dev:armhf pkg-config && \ cargo install --force --locked bindgen-cli && \ SYSROOT=$(arm-linux-gnueabihf-gcc -print-sysroot); \ echo "--sysroot=$SYSROOT -I$SYSROOT/usr/include -I$SYSROOT/usr/include/arm-linux-gnueabihf" > BINDGEN_EXTRA_CLANG_ARGS; \ LIBDIR="/lib/arm-linux-gnueabihf"; \ USRLIBDIR="/usr/lib/arm-linux-gnueabihf"; \ + HOST_TRIPLE="arm-linux-gnueabihf"; \ else \ echo "Unsupported cross compilation target: $TARGETARCH"; \ exit 1; \ fi && \ - cp $LIBDIR/libgcc_s.so.1 $USRLIBDIR/libodbc.so.2 $USRLIBDIR/libltdl.so.7 /opt/sqlpage-libs/ && \ + echo $USRLIBDIR > ODBC_LIBDIR && \ + cp $LIBDIR/libgcc_s.so.1 /opt/sqlpage-libs/ && \ rustup target add $(cat TARGET) && \ cargo init . # Build dependencies (creates a layer that avoids recompiling dependencies on every build) COPY Cargo.toml Cargo.lock ./ RUN BINDGEN_EXTRA_CLANG_ARGS=$(cat BINDGEN_EXTRA_CLANG_ARGS || true) \ + RS_ODBC_LINK_SEARCH=$(cat ODBC_LIBDIR) \ cargo build \ --target $(cat TARGET) \ --config target.$(cat TARGET).linker='"'$(cat LINKER)'"' \ @@ -46,6 +52,7 @@ RUN BINDGEN_EXTRA_CLANG_ARGS=$(cat BINDGEN_EXTRA_CLANG_ARGS || true) \ # Build the project COPY . . RUN touch src/main.rs && \ + RS_ODBC_LINK_SEARCH=$(cat ODBC_LIBDIR) \ cargo build \ --target $(cat TARGET) \ --config target.$(cat TARGET).linker='"'$(cat LINKER)'"' \ @@ -62,6 +69,7 @@ ENV SQLPAGE_WEB_ROOT=/var/www ENV SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage WORKDIR /var/www COPY --from=builder /usr/src/sqlpage/sqlpage.bin /usr/local/bin/sqlpage +# Provide runtime helper libs in system lib directory for the glibc busybox base COPY --from=builder /opt/sqlpage-libs/* /lib/ USER sqlpage COPY --from=builder --chown=sqlpage:sqlpage /usr/src/sqlpage/sqlpage/sqlpage.db sqlpage/sqlpage.db diff --git a/README.md b/README.md index 69f365e0..c25c8867 100644 --- a/README.md +++ b/README.md @@ -190,13 +190,9 @@ You can skip this section if you want to use one of the built-in database driver SQLPage supports ODBC connections to connect to databases that don't have native drivers, such as Oracle, Snowflake, BigQuery, IBM DB2, and many others. -ODBC support requires an ODBC driver manager and appropriate database drivers to be installed on your system. - -#### Install ODBC - - - On windows, it's installed by default. - - On linux: `sudo apt-get install -y unixodbc odbcinst unixodbc-common libodbcinst2` - - On mac: `brew install unixodbc` +On Linux, SQLPage supports both dynamic and static ODBC linking. The Docker image uses the system `unixODBC` (dynamic). +Linux and MacOS release binaries are built with a statically linked unixODBC. +You still need to install or provide the database-specific ODBC driver for the database you want to connect to. #### Install your ODBC database driver @@ -207,7 +203,9 @@ ODBC support requires an ODBC driver manager and appropriate database drivers to #### Connect to your database - - Find your [connection string](https://www.connectionstrings.com/). It will look like this: `Driver={SnowflakeDSIIDriver};Server=xyz.snowflakecomputing.com;Database=MY_DB;Schema=PUBLIC;UID=my_user;PWD=my_password` + - Find your [connection string](https://www.connectionstrings.com/). + - It will look like this: `Driver=/opt/snowflake_odbc/lib/libSnowflake.so;Server=xyz.snowflakecomputing.com;Database=MY_DB;Schema=PUBLIC;UID=my_user;PWD=my_password` + - It must reference the path to the database driver you installed earlier, plus any connection parameter required by the driver itself. Follow the instructions from the driver's own documentation. - Use it in the [DATABASE_URL configuration option](./configuration.md) diff --git a/configuration.md b/configuration.md index 5be0f1ca..92589c42 100644 --- a/configuration.md +++ b/configuration.md @@ -105,7 +105,7 @@ DATABASE_URL="Driver={Oracle ODBC Driver};Server=localhost:1521/XE;UID=hr;PWD=pa DATABASE_URL="Driver={SnowflakeDSIIDriver};Server=account.snowflakecomputing.com;Database=mydb;UID=user;PWD=password" ``` -ODBC drivers must be installed and configured on your system. On Linux, you typically need `unixodbc` and the appropriate database-specific ODBC driver. +ODBC drivers must be installed and configured on your system. On Linux, the `unixODBC` driver manager is statically linked into the SQLPage binary, so you usually only need to install and configure the database-specific ODBC driver for your target database (for example Snowflake, Oracle, DuckDB...). If the `database_password` configuration parameter is set, it will override any password specified in the `database_url`. It does not need to be percent-encoded. diff --git a/docker-compose.yml b/docker-compose.yml index 0d92fb83..b67355d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ # DATABASE_URL='postgres://root:Password123!@localhost/sqlpage' # DATABASE_URL='mssql://root:Password123!@localhost/sqlpage' # DATABASE_URL='mysql://root:Password123!@localhost/sqlpage' +# DATABASE_URL='Driver={/usr/lib64/psqlodbcw.so};Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!' # Run for instance: # docker compose up postgres diff --git a/examples/official-site/assets/db-bigquery.svg b/examples/official-site/assets/db-bigquery.svg new file mode 100644 index 00000000..21fbe250 --- /dev/null +++ b/examples/official-site/assets/db-bigquery.svg @@ -0,0 +1 @@ +Google BigQuery diff --git a/examples/official-site/assets/db-clickhouse.svg b/examples/official-site/assets/db-clickhouse.svg new file mode 100644 index 00000000..82d7d981 --- /dev/null +++ b/examples/official-site/assets/db-clickhouse.svg @@ -0,0 +1 @@ +ClickHouse diff --git a/examples/official-site/assets/db-databricks.svg b/examples/official-site/assets/db-databricks.svg new file mode 100644 index 00000000..129ea2ac --- /dev/null +++ b/examples/official-site/assets/db-databricks.svg @@ -0,0 +1 @@ +Databricks diff --git a/examples/official-site/assets/db-db2.svg b/examples/official-site/assets/db-db2.svg new file mode 100644 index 00000000..02811396 --- /dev/null +++ b/examples/official-site/assets/db-db2.svg @@ -0,0 +1 @@ +IBM \ No newline at end of file diff --git a/examples/official-site/assets/db-duckdb.svg b/examples/official-site/assets/db-duckdb.svg new file mode 100644 index 00000000..7e590871 --- /dev/null +++ b/examples/official-site/assets/db-duckdb.svg @@ -0,0 +1 @@ +DuckDB diff --git a/examples/official-site/assets/db-mysql.svg b/examples/official-site/assets/db-mysql.svg new file mode 100644 index 00000000..e1606ffd --- /dev/null +++ b/examples/official-site/assets/db-mysql.svg @@ -0,0 +1 @@ +MySQL diff --git a/examples/official-site/assets/db-odbc.svg b/examples/official-site/assets/db-odbc.svg new file mode 100644 index 00000000..b364b5a6 --- /dev/null +++ b/examples/official-site/assets/db-odbc.svg @@ -0,0 +1 @@ +ODBCODBC diff --git a/examples/official-site/assets/db-oracle.svg b/examples/official-site/assets/db-oracle.svg new file mode 100644 index 00000000..1e41072f --- /dev/null +++ b/examples/official-site/assets/db-oracle.svg @@ -0,0 +1 @@ +Oracle \ No newline at end of file diff --git a/examples/official-site/assets/db-postgres.svg b/examples/official-site/assets/db-postgres.svg new file mode 100644 index 00000000..d7ccd9e3 --- /dev/null +++ b/examples/official-site/assets/db-postgres.svg @@ -0,0 +1 @@ +PostgreSQL diff --git a/examples/official-site/assets/db-snowflake.svg b/examples/official-site/assets/db-snowflake.svg new file mode 100644 index 00000000..b62af544 --- /dev/null +++ b/examples/official-site/assets/db-snowflake.svg @@ -0,0 +1 @@ +Snowflake diff --git a/examples/official-site/assets/db-sqlite.svg b/examples/official-site/assets/db-sqlite.svg new file mode 100644 index 00000000..e6e77901 --- /dev/null +++ b/examples/official-site/assets/db-sqlite.svg @@ -0,0 +1 @@ +SQLite diff --git a/examples/official-site/assets/db-sqlserver.svg b/examples/official-site/assets/db-sqlserver.svg new file mode 100644 index 00000000..ecb4c222 --- /dev/null +++ b/examples/official-site/assets/db-sqlserver.svg @@ -0,0 +1 @@ +Microsoft SQL Server \ No newline at end of file diff --git a/examples/official-site/sqlpage/templates/shell-home.handlebars b/examples/official-site/sqlpage/templates/shell-home.handlebars index f5839f09..095c6b73 100644 --- a/examples/official-site/sqlpage/templates/shell-home.handlebars +++ b/examples/official-site/sqlpage/templates/shell-home.handlebars @@ -495,7 +495,7 @@ font-weight: 500; background: var(--gradient-primary); color: rgba(255, 255, 255, 0.95); - text-decoration: none; + text-decoration: none !important; border-radius: 12px; transition: all var(--transition-slow); border: 1px solid rgba(255, 255, 255, 0.1); @@ -803,6 +803,343 @@ } } + .compat-section { + position: relative; + background: radial-gradient(circle at 20% 20%, rgba(88, 125, 255, 0.18), transparent 50%), radial-gradient(circle at 80% 10%, rgba(255, 130, 255, 0.18), transparent 55%), linear-gradient(160deg, rgba(10, 19, 44, 0.95), rgba(7, 12, 26, 0.92)); + overflow: hidden; + isolation: isolate; + } + + .compat-section::before, + .compat-section::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + } + + .compat-section::before { + background-image: radial-gradient(2px 2px at 25px 35px, rgba(255, 255, 255, 0.6), transparent), radial-gradient(1px 1px at 120px 80px, rgba(255, 255, 255, 0.4), transparent), radial-gradient(1.5px 1.5px at 220px 140px, rgba(255, 255, 255, 0.55), transparent), radial-gradient(2px 2px at 320px 60px, rgba(255, 255, 255, 0.5), transparent), radial-gradient(1px 1px at 420px 180px, rgba(255, 255, 255, 0.4), transparent), radial-gradient(1.5px 1.5px at 520px 90px, rgba(255, 255, 255, 0.5), transparent); + background-size: 240px 220px; + opacity: 0.45; + animation: twinkle 14s linear infinite; + } + + .compat-section::after { + background: radial-gradient(450px 450px at 80% 110%, rgba(63, 104, 255, 0.35), transparent 70%); + } + + .compat-layout { + display: grid; + grid-template-columns: minmax(400px, 1fr) minmax(450px, 1.2fr); + gap: var(--gap-lg); + padding-top: calc(var(--container-padding) * 1.5); + padding-bottom: calc(var(--container-padding) * 1.5); + position: relative; + z-index: 1; + } + + .compat-intro { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .compat-eyebrow { + font-size: 0.85rem; + letter-spacing: 0.3em; + text-transform: uppercase; + color: rgba(173, 188, 255, 0.75); + } + + .compat-intro h2 { + font-size: 3.2rem; + line-height: 1.1; + color: rgba(229, 237, 255, 0.94); + } + + .compat-intro p { + font-size: 1.15rem; + color: rgba(209, 219, 255, 0.78); + max-width: 90%; + } + + .compat-highlights { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + } + + .compat-pill { + padding: 0.45rem 0.9rem; + border-radius: 999px; + background: rgba(99, 118, 255, 0.12); + border: 1px solid rgba(143, 162, 255, 0.25); + color: rgba(207, 221, 255, 0.78); + font-size: 0.85rem; + backdrop-filter: blur(6px); + } + + .compat-showcase { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; + width: 100%; + min-width: 0; + } + + .compat-orbit { + position: relative; + width: 320px; + height: 320px; + margin: 0 auto; + border-radius: 50%; + background: radial-gradient(circle, rgba(64, 84, 182, 0.35), rgba(19, 27, 51, 0.4)); + box-shadow: inset 0 0 40px rgba(127, 162, 255, 0.2), 0 20px 60px rgba(6, 12, 32, 0.7); + } + + .compat-orbit::before { + content: ""; + position: absolute; + inset: 14px; + border-radius: 50%; + border: 1px dashed rgba(162, 186, 255, 0.4); + animation: orbitSpin 26s linear infinite; + } + + .compat-orbit::after { + content: ""; + position: absolute; + inset: 44px; + border-radius: 50%; + border: 1px solid rgba(86, 112, 221, 0.2); + filter: blur(0.3px); + } + + .compat-core { + position: absolute; + width: 160px; + height: 160px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, rgba(35, 46, 98, 0.95), rgba(20, 30, 70, 0.9)); + box-shadow: 0 0 40px rgba(150, 178, 255, 0.32); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 1rem; + color: rgba(229, 236, 255, 0.95); + font-size: 0.9rem; + line-height: 1.3; + backdrop-filter: blur(6px); + margin: auto; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + .compat-node { + position: absolute; + width: 84px; + height: 84px; + border-radius: 18px; + background: rgba(10, 17, 36, 0.72); + box-shadow: 0 12px 28px rgba(2, 6, 18, 0.6), inset 0 0 24px rgba(137, 157, 255, 0.24); + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10px); + border: 1px solid rgba(143, 166, 255, 0.35); + outline: 1px solid rgba(12, 18, 38, 0.6); + outline-offset: -4px; + } + + .compat-node img { + width: 54px; + height: 54px; + } + + .compat-node:nth-child(2) { + top: 6%; + left: 50%; + transform: translate(-50%, -50%); + animation: floatNode 7s ease-in-out infinite; + } + + .compat-node:nth-child(3) { + top: 50%; + right: 2%; + transform: translate(50%, -50%); + animation: floatNode 9s ease-in-out infinite; + } + + .compat-node:nth-child(4) { + bottom: 4%; + left: 50%; + transform: translate(-50%, 50%); + animation: floatNode 8s ease-in-out infinite; + } + + .compat-node:nth-child(5) { + top: 50%; + left: 2%; + transform: translate(-50%, -50%); + animation: floatNode 10s ease-in-out infinite; + } + + .compat-card { + background: rgba(12, 20, 42, 0.68); + border-radius: 18px; + padding: 1.8rem; + border: 1px solid rgba(110, 138, 238, 0.32); + box-shadow: 0 18px 40px rgba(4, 8, 22, 0.6); + backdrop-filter: blur(12px); + display: grid; + gap: 1.2rem; + width: 100%; + max-width: 500px; + min-width: 380px; + } + + .compat-card h3 { + font-size: 1.35rem; + color: rgba(228, 236, 255, 0.92); + } + + .compat-card p { + font-size: 0.98rem; + color: rgba(204, 214, 255, 0.72); + line-height: 1.6; + } + + .compat-logos { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; + } + + .compat-logos .db-logo { + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + padding: 0.7rem; + background: rgba(19, 30, 58, 0.9); + border: 1px solid rgba(118, 140, 233, 0.25); + position: relative; + overflow: hidden; + } + + .compat-logos img { + width: 44px; + height: 44px; + } + + .compat-badges { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 0.75rem; + } + + .compat-badge { + padding: 0.35rem 0.75rem; + border-radius: 999px; + background: rgba(57, 76, 162, 0.32); + color: rgba(225, 232, 255, 0.78); + font-size: 0.82rem; + } + + @keyframes twinkle { + 0% { + opacity: 0.4; + transform: scale(1) translate3d(0, 0, 0); + } + + 50% { + opacity: 0.75; + transform: scale(1.05) translate3d(-2%, -3%, 0); + } + + 100% { + opacity: 0.4; + transform: scale(1) translate3d(0, 0, 0); + } + } + + @keyframes orbitSpin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + @keyframes floatNode { + 0% { + transform: translate3d(0, 0, 0); + } + + 50% { + transform: translate3d(0, -10px, 0); + } + + 100% { + transform: translate3d(0, 0, 0); + } + } + + @media (max-width: 968px) { + .compat-layout { + grid-template-columns: 1fr; + text-align: center; + } + + .compat-intro p { + max-width: 100%; + } + + .compat-highlights { + justify-content: center; + } + + .compat-orbit { + width: 260px; + height: 260px; + } + + .compat-node { + width: 74px; + height: 74px; + } + + .compat-node img { + width: 46px; + height: 46px; + } + + .compat-logos { + grid-template-columns: repeat(3, 1fr); + } + } + + @media (max-width: 640px) { + .compat-intro h2 { + font-size: 2.6rem; + } + + .compat-logos { + grid-template-columns: repeat(2, 1fr); + } + + .compat-card { + padding: 1.4rem; + } + } + /* FOOTER STYLES */ footer { background: linear-gradient(135deg, @@ -1041,7 +1378,7 @@
-

More scalable than a spreadsheet

+

More scalable than a spreadsheet

SQL queries sort, filter, and aggregate millions of rows in milliseconds. No more slow spreadsheet formulas or memory limitations. Your app remains smooth and responsive even @@ -1074,6 +1411,56 @@

+
+
+
+
Database Compatibility
+

Works with your database

+

SQLPage connects to the database engine you already rely on today. + Keep your data in place and surface it through a + friendly interface that stays in sync. + If you don't have a DB yet, SQLPage comes with a built-in query engine. +

+
+
SQLite built-in
+
MySQL & MariaDB
+
PostgreSQL family
+
Microsoft SQL Server
+
ODBC bridge
+
+
+
+
+
+ Native connectors +
+
SQLite
+
MySQL
+
PostgreSQL
+
Microsoft SQL Server
+
+
+

Wherever your data lives

+

Through ODBC you can plug SQLPage into any warehouses and enterprise engines.

+
+ + + + + + + + +
+
+ No data copy + Streams query results +
+
+
+
+
+
diff --git a/examples/official-site/your-first-sql-website/tutorial.md b/examples/official-site/your-first-sql-website/tutorial.md index 79819136..6faaaf1c 100644 --- a/examples/official-site/your-first-sql-website/tutorial.md +++ b/examples/official-site/your-first-sql-website/tutorial.md @@ -91,6 +91,7 @@ Later, when you want to deploy your website online, you can switch back to a per - a PostgreSQL-compatible server with `postgres://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/postgres/struct.PgConnectOptions.html)), - a MySQL-compatible server with `mysql://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/mysql/struct.MySqlConnectOptions.html)), - a Microsoft SQL Server with `mssql://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/mssql/struct.MssqlConnectOptions.html#method.from_str), [note about named instances](https://github.com/sqlpage/SQLPage/issues/92)), +- any ODBC-compatible database like DuckDB, ClickHouse, Databricks, Snowflake, BigQuery, Oracle, Db2, and many more. See [ODBC database connection instructions](https://github.com/sqlpage/SQLPage#odbc-setup). > If `user` or `password` **contains special characters**, you should [**percent-encode**](https://en.wikipedia.org/wiki/Percent-encoding) them. > diff --git a/src/webserver/database/connect.rs b/src/webserver/database/connect.rs index b67dd431..09528b0c 100644 --- a/src/webserver/database/connect.rs +++ b/src/webserver/database/connect.rs @@ -8,9 +8,9 @@ use crate::{ }; use anyhow::Context; use futures_util::future::BoxFuture; +use sqlx::odbc::OdbcConnectOptions; use sqlx::{ any::{Any, AnyConnectOptions, AnyKind}, - odbc::OdbcConnectOptions, pool::PoolOptions, sqlite::{Function, SqliteConnectOptions, SqliteFunctionCtx}, ConnectOptions, Connection, Executor, @@ -209,6 +209,7 @@ fn set_custom_connect_options(options: &mut AnyConnectOptions, config: &AppConfi if let Some(sqlite_options) = options.as_sqlite_mut() { set_custom_connect_options_sqlite(sqlite_options, config); } + if let Some(odbc_options) = options.as_odbc_mut() { set_custom_connect_options_odbc(odbc_options, config); }