├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── LICENSE.txt ├── README.md ├── deps.edn ├── resources └── metabase-plugin.yaml └── src └── metabase └── driver └── duckdb.clj /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster 2 | ARG VARIANT=17 3 | FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} 4 | 5 | # [Optional] Clojure version 6 | ARG CLOJURE_VERSION=1.10.3 7 | 8 | # [Optional] Clojure tools version 9 | ARG CLOJURE_CLI_VERSION=1.10.3.1075 10 | 11 | # [Optional] Leiningen version 12 | ARG LEININGEN_VERSION="stable" 13 | 14 | # [Optional] POLYLITH version 15 | ARG POLYLITH_VERSION="0.2.13-alpha" 16 | 17 | # [Optional] Boot version 18 | ENV BOOT_VERSION=2.8.3 19 | 20 | # [Optional] Clojure version used by Boot 21 | ENV BOOT_CLOJURE_VERSION=${CLOJURE_VERSION} 22 | 23 | # [Option] Install Clojure CLI tool 24 | ARG INSTALL_CLOJURE_CLI="true" 25 | 26 | # [Option] Install Boot 27 | ARG INSTALL_BOOT="true" 28 | 29 | # [Option] Install Leiningen 30 | ARG INSTALL_LEININGEN="true" 31 | 32 | # [Option] Install Polylith 33 | ARG INSTALL_POLYLITH="true" 34 | 35 | RUN if [ "${INSTALL_CLOJURE_CLI}" = "true" ]; then \ 36 | apt-get update \ 37 | && apt-get -y install rlwrap \ 38 | && curl -OL "https://download.clojure.org/install/linux-install-${CLOJURE_CLI_VERSION}.sh" \ 39 | && chmod +x linux-install-${CLOJURE_CLI_VERSION}.sh \ 40 | && /linux-install-${CLOJURE_CLI_VERSION}.sh \ 41 | && rm /linux-install-${CLOJURE_CLI_VERSION}.sh \ 42 | && su vscode -c "clj --version"; fi 43 | 44 | RUN if [ "${INSTALL_BOOT}" = "true" ]; then \ 45 | curl -OL "https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh" \ 46 | && chmod +x boot.sh \ 47 | && mv boot.sh /usr/local/sbin/boot \ 48 | && su vscode -c "boot -u"; fi 49 | 50 | RUN if [ "${INSTALL_LEININGEN}" = "true" ]; then \ 51 | curl -OL "https://raw.githubusercontent.com/technomancy/leiningen/${LEININGEN_VERSION}/bin/lein" \ 52 | && chmod +x lein \ 53 | && mv lein /usr/local/sbin; fi 54 | 55 | # Cache Clojure and dependencies 56 | RUN if [ "${INSTALL_LEININGEN}" = "true" ]; then \ 57 | su vscode -c " cd ~ \ 58 | && echo '(defproject dummy \"\" :dependencies [[org.clojure/clojure \"'${CLOJURE_VERSION}'\"]])' > project.clj \ 59 | && lein deps \ 60 | && rm project.clj"; fi 61 | 62 | RUN if [ "${INSTALL_POLYLITH}" = "true" ]; then \ 63 | curl -OL "https://github.com/polyfy/polylith/releases/download/v${POLYLITH_VERSION}/poly-${POLYLITH_VERSION}.jar" \ 64 | && mkdir -p /usr/local/polylith \ 65 | && mv poly-$POLYLITH_VERSION.jar /usr/local/polylith \ 66 | && echo '#!/bin/sh\nARGS=""\nwhile [ "$1" != "" ] ; do\n ARGS="$ARGS $1"\n shift\ndone\nexec "java" $JVM_OPTS "-jar" "/usr/local/polylith/poly-'$POLYLITH_VERSION'.jar" $ARGS\n' > /usr/local/sbin/poly \ 67 | && chmod +x /usr/local/sbin/poly \ 68 | && /usr/local/sbin/poly version; fi 69 | 70 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 71 | ARG NODE_VERSION="lts/*" 72 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 73 | 74 | # [Optional] Uncomment this section to install additional OS packages. 75 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 76 | # && apt-get -y install --no-install-recommends 77 | 78 | # [Optional] Uncomment this line to install global node packages. 79 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 80 | 81 | # Clean up package lists 82 | RUN apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 83 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/clojure 3 | { 4 | "name": "Clojure (Community)", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a Java version. 9 | // Append -bullseye or -buster to pin to an OS version. 10 | // Use the -bullseye variants on local arm64/Apple Silicon. 11 | "VARIANT": "17", 12 | // Options 13 | "CLOJURE_VERSION": "1.10.3", 14 | "INSTALL_CLOJURE_CLI": "true", 15 | "INSTALL_BOOT": "true", 16 | "INSTALL_LEININGEN": "true", 17 | "INSTALL_POLYLITH": "true", 18 | "NODE_VERSION": "lts/*" 19 | } 20 | }, 21 | 22 | // Configure tool-specific properties. 23 | "customizations": { 24 | // Configure properties specific to VS Code. 25 | "vscode": { 26 | // Set *default* container specific settings.json values on container create. 27 | "settings": { 28 | }, 29 | 30 | // Add the IDs of extensions you want installed when the container is created. 31 | "extensions": [ 32 | "vscjava.vscode-java-pack", 33 | "borkdude.clj-kondo", 34 | "betterthantomorrow.calva" 35 | ] 36 | } 37 | }, 38 | 39 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 40 | // "forwardPorts": [], 41 | 42 | // Use 'postCreateCommand' to run commands after the container is created. 43 | // "postCreateCommand": "java -version", 44 | 45 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 46 | "remoteUser": "vscode" 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \#*\# 2 | .\#* 3 | /target 4 | /.nrepl-port 5 | /bin 6 | .classpath 7 | .project 8 | .idea 9 | *.iml 10 | duckdb-driver.iml 11 | .lein-repl-history 12 | .cpcache 13 | .clj-kondo 14 | .lsp 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archived 2 | **This repository has moved to new maintainer https://github.com/MotherDuck-Open-Source/metabase_duckdb_driver** 3 | 4 | # Metabase DuckDB Driver (Community-Supported) 5 | 6 | The Metabase DuckDB allows Metabase SNAPSHOT to use embedded DuckDB database. 7 | 8 | This driver is community driver and is not considered part of the 9 | core Metabase project. If you would like to open a GitHub issue to 10 | report a bug or request new features, or would like to open a pull 11 | requests against it, please do so in this repository, and not in the 12 | core Metabase GitHub repository. 13 | 14 | ## DuckDB 15 | 16 | [DuckDB](https://duckdb.org) is an in-process SQL OLAP database management. It does not run as a separate process, but completely embedded within a host process. So, it **embedds to the Metabase process** like Sqlite. 17 | 18 | ## Obtaining the DuckDB Metabase driver 19 | 20 | ### Where to find it 21 | 22 | [Click here](https://github.com/AlexR2D2/metabase_duckdb_driver/releases/latest) to view the latest release of the Metabase DuckDB driver; click the link to download `duckdb.metabase-driver.jar`. 23 | 24 | You can find past releases of the DuckDB driver [here](https://github.com/AlexR2D2/metabase_duckdb_driver/releases). 25 | 26 | ### How to Install it 27 | 28 | Metabase will automatically make the DuckDB driver available if it finds the driver in the Metabase plugins directory when it starts up. 29 | All you need to do is create the directory `plugins` (if it's not already there), move the JAR you just downloaded into it, and restart Metabase. 30 | 31 | By default, the plugins directory is called `plugins`, and lives in the same directory as the Metabase JAR. 32 | 33 | For example, if you're running Metabase from a directory called `/app/`, you should move the DuckDB driver to `/app/plugins/`: 34 | 35 | ```bash 36 | # example directory structure for running Metabase with DuckDB support 37 | /app/metabase.jar 38 | /app/plugins/duckdb.metabase-driver.jar 39 | ``` 40 | 41 | If you're running Metabase from the Mac App, the plugins directory defaults to `~/Library/Application Support/Metabase/Plugins/`: 42 | 43 | ```bash 44 | # example directory structure for running Metabase Mac App with DuckDB support 45 | /Users/you/Library/Application Support/Metabase/Plugins/duckdb.metabase-driver.jar 46 | ``` 47 | 48 | If you are running the Docker image or you want to use another directory for plugins, you should specify a custom plugins directory by setting the environment variable `MB_PLUGINS_DIR`. 49 | 50 | ## Configuring 51 | 52 | Once you've started up Metabase, go to add a database and select "DuckDB". Provide the path to the DuckDB database file. if you don't specify a path DuckDB will be started in memory mode without any data at all. 53 | 54 | ## Parquet 55 | 56 | Does it make sense to start DuckDB Database in memory mode without any data in system like Metabase? Of Course yes! 57 | Because of feature of DuckDB allowing you [to run SQL queries directly on Parquet files](https://duckdb.org/2021/06/25/querying-parquet.html). So, you don't need a DuckDB database. 58 | 59 | For example (somewhere in Metabase SQL Query editor): 60 | 61 | ```sql 62 | # DuckDB selected as source 63 | 64 | SELECT originalTitle, startYear, genres, numVotes, averageRating from '/Users/you/movies/title.basics.parquet' x 65 | JOIN (SELECT * from '/Users/you/movies/title.ratings.parquet') y ON x.tconst = y.tconst 66 | ORDER BY averageRating * numVotes DESC 67 | ``` 68 | 69 | ## Docker 70 | 71 | Unfortunately, DuckDB plugin does't work in the default Alpine based Metabase docker container due to some glibc problems. But thanks to @ChrisH and @lucmartinon we have simple Dockerfile to create Docker image of Metabase based on Debian where the DuckDB plugin does work. 72 | 73 | ```bash 74 | FROM openjdk:19-buster 75 | 76 | ENV MB_PLUGINS_DIR=/home/plugins/ 77 | 78 | ADD https://downloads.metabase.com/v0.46.2/metabase.jar /home 79 | ADD https://github.com/AlexR2D2/metabase_duckdb_driver/releases/download/0.1.6/duckdb.metabase-driver.jar /home/plugins/ 80 | 81 | RUN chmod 744 /home/plugins/duckdb.metabase-driver.jar 82 | 83 | CMD ["java", "-jar", "/home/metabase.jar"] 84 | ``` 85 | 86 | Build the image: 87 | ```bash 88 | docker build . --tag metaduck:latest` 89 | ``` 90 | 91 | Then create the container: 92 | ```bash 93 | docker run --name metaduck -d -p 80:3000 -m 2GB -e MB_PLUGINS_DIR=/home/plugins metaduck 94 | ``` 95 | 96 | Open Metabase in the browser: http://localhost 97 | 98 | ### Using DB file with Docker 99 | 100 | In order to use the DuckDB database file from your local host in the docker container you should mount folder with your DB file into docker container 101 | 102 | ```bash 103 | docker run -v /dir_with_my_duck_db_file_in_the_local_host/:/container/directory ... 104 | ``` 105 | 106 | Next, in the settings page of DuckDB of Metabase Web UI you could set your DB file name like this 107 | 108 | ```bash 109 | /container/directory/ 110 | ``` 111 | 112 | The same way you could mount the dir with parquet files into container and make SQL queries to this files using directory in your container. 113 | 114 | ## How to build the DuckDB .jar plugin yourself 115 | 116 | 1. Install VS Code with [DevContainer](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension (see [details](https://code.visualstudio.com/docs/devcontainers/containers)) 117 | 2. Create some folder, let's say `duckdb_plugin` 118 | 3. Clone the `metabase_duckdb_driver` repository into `duckdb_plugin` folder 119 | 4. Copy `.devcontainer` from `duckdb_plugin/metabase_duckdb_driver` into `duckdb_plugin` 120 | 5. Clone the `metabase` repository of version you need into `duckdb_plugin` folder 121 | 6. Now content of the `duckdb_plugin` folder should looks like this: 122 | ``` 123 | .. 124 | .devcontainer 125 | metabase 126 | metabase_duckdb_driver 127 | ``` 128 | 7. Add duckdb record to the deps file `duckdb_plugin/metabase/modules/drivers/deps.edn` 129 | The end of the file sholud looks like this: 130 | ``` 131 | ... 132 | metabase/sqlserver {:local/root "sqlserver"} 133 | metabase/vertica {:local/root "vertica"} 134 | metabase/duckdb {:local/root "duckdb"}}} <- add this! 135 | ``` 136 | 8. Set the DuckDB version you need in the `duckdb_plugin/metabase_duckdb_driver/deps.edn` 137 | 9. Create duckdb driver directory in the cloned metabase sourcecode: 138 | ``` 139 | > mkdir -p duckdb_plugin/metabase/modules/drivers/duckdb 140 | ``` 141 | 10. Copy the `metabase_duckdb_driver` source code into created dir 142 | ``` 143 | > cp -rf duckdb_plugin/metabase_duckdb_driver/* duckdb_plugin/metabase/modules/drivers/duckdb/ 144 | ``` 145 | 11. Open `duckdb_plugin` folder in VSCode using DevContainer extension (vscode will offer to open this folder using devcontainer). Wait until all stuff will be loaded. At the end you will get the terminal opened directly in the VS Code, smth like this: 146 | ``` 147 | vscode ➜ /workspaces/duckdb_plugin $ 148 | ``` 149 | 12. Build the plugin 150 | ``` 151 | vscode ➜ /workspaces/duckdb_plugin $ cd metabase 152 | vscode ➜ /workspaces/duckdb_plugin $ clojure -X:build:drivers:build/driver :driver :duckdb 153 | ``` 154 | 13. jar file of DuckDB plugin will be generated here duckdb_plugin/metabase/resources/modules/duckdb.metabase-driver.jar 155 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src" "resources"] 3 | 4 | :deps 5 | {org.duckdb/duckdb_jdbc {:mvn/version "0.10.0"}}} 6 | -------------------------------------------------------------------------------- /resources/metabase-plugin.yaml: -------------------------------------------------------------------------------- 1 | info: 2 | name: Metabase DuckDB Driver 3 | version: 1.0.0-SNAPSHOT-0.1.13 4 | description: Allows Metabase to connect to DuckDB databases. 5 | contact-info: 6 | name: Alexander Golubov 7 | address: golubov.ax@yandex.ru 8 | driver: 9 | name: duckdb 10 | display-name: DuckDB 11 | lazy-load: true 12 | parent: sql-jdbc 13 | connection-properties: 14 | - name: database_file 15 | display-name: Database file 16 | placeholder: /home/you/the.duckdb (or empty for 'in memory' mode) 17 | required: false 18 | - name: read_only 19 | display-name: Establish a read-only connection 20 | default: false 21 | type: boolean 22 | - name: old_implicit_casting 23 | display-name: Use old_implicit_casting 24 | default: false 25 | type: boolean 26 | 27 | init: 28 | - step: load-namespace 29 | namespace: metabase.driver.duckdb 30 | - step: register-jdbc-driver 31 | class: org.duckdb.DuckDBDriver 32 | -------------------------------------------------------------------------------- /src/metabase/driver/duckdb.clj: -------------------------------------------------------------------------------- 1 | (ns metabase.driver.duckdb 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [medley.core :as m] 4 | [metabase.driver :as driver] 5 | [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] 6 | [metabase.driver.sql-jdbc.execute :as sql-jdbc.execute] 7 | [metabase.driver.sql-jdbc.sync :as sql-jdbc.sync] 8 | [metabase.driver.sql.query-processor :as sql.qp] 9 | [metabase.util.honey-sql-2 :as hx]) 10 | (:import [java.sql Statement ResultSet ResultSetMetaData Types])) 11 | 12 | (driver/register! :duckdb, :parent :sql-jdbc) 13 | 14 | (defmethod sql-jdbc.conn/connection-details->spec :duckdb 15 | [_ {:keys [database_file, read_only, old_implicit_casting], :as details}] 16 | (let [conn_details (merge 17 | {:classname "org.duckdb.DuckDBDriver" 18 | :subprotocol "duckdb" 19 | :subname (or database_file "") 20 | "duckdb.read_only" (str read_only) 21 | "old_implicit_casting" (str old_implicit_casting)} 22 | (dissoc details :database_file :read_only :port :engine))] 23 | conn_details)) 24 | 25 | (defmethod sql.qp/honey-sql-version :duckdb 26 | [_driver] 27 | 2) 28 | 29 | (def ^:private database-type->base-type 30 | (sql-jdbc.sync/pattern-based-database-type->base-type 31 | [[#"BOOLEAN" :type/Boolean] 32 | [#"BOOL" :type/Boolean] 33 | [#"LOGICAL" :type/Boolean] 34 | [#"HUGEINT" :type/BigInteger] 35 | [#"BIGINT" :type/BigInteger] 36 | [#"UBIGINT" :type/BigInteger] 37 | [#"INT8" :type/BigInteger] 38 | [#"LONG" :type/BigInteger] 39 | [#"INT" :type/Integer] 40 | [#"INTEGER" :type/Integer] 41 | [#"INT4" :type/Integer] 42 | [#"SIGNED" :type/Integer] 43 | [#"SMALLINT" :type/Integer] 44 | [#"INT2" :type/Integer] 45 | [#"SHORT" :type/Integer] 46 | [#"TINYINT" :type/Integer] 47 | [#"INT1" :type/Integer] 48 | [#"UINTEGER" :type/Integer] 49 | [#"USMALLINT" :type/Integer] 50 | [#"UTINYINT" :type/Integer] 51 | [#"DECIMAL" :type/Decimal] 52 | [#"DOUBLE" :type/Float] 53 | [#"FLOAT8" :type/Float] 54 | [#"NUMERIC" :type/Float] 55 | [#"REAL" :type/Float] 56 | [#"FLOAT4" :type/Float] 57 | [#"FLOAT" :type/Float] 58 | [#"VARCHAR" :type/Text] 59 | [#"CHAR" :type/Text] 60 | [#"BPCHAR" :type/Text] 61 | [#"TEXT" :type/Text] 62 | [#"STRING" :type/Text] 63 | [#"BLOB" :type/*] 64 | [#"BYTEA" :type/*] 65 | [#"BINARY" :type/*] 66 | [#"VARBINARY" :type/*] 67 | [#"UUID" :type/UUID] 68 | [#"TIMESTAMP" :type/DateTime] 69 | [#"DATETIME" :type/DateTime] 70 | [#"TIMESTAMPTZ" :type/DateTimeWithZoneOffset] 71 | [#"DATE" :type/Date] 72 | [#"TIME" :type/Time]])) 73 | 74 | (defmethod sql-jdbc.sync/database-type->base-type :duckdb 75 | [_ field-type] 76 | (database-type->base-type field-type)) 77 | 78 | ;; .getObject of DuckDB (v0.4.0) does't handle the java.time.LocalDate but sql.Date only, 79 | ;; so get the sql.Date from DuckDB and convert it to java.time.LocalDate 80 | (defmethod sql-jdbc.execute/read-column-thunk [:duckdb Types/DATE] 81 | [_ ^ResultSet rs _rsmeta ^Integer i] 82 | (fn [] 83 | (when-let [sqlDate (.getDate rs i)] 84 | (.toLocalDate sqlDate)))) 85 | 86 | ;; .getObject of DuckDB (v0.4.0) does't handle the java.time.LocalTime but sql.Time only, 87 | ;; so get the sql.Time from DuckDB and convert it to java.time.LocalTime 88 | (defmethod sql-jdbc.execute/read-column-thunk [:duckdb Types/TIME] 89 | [_ ^ResultSet rs _rsmeta ^Integer i] 90 | (fn [] 91 | (when-let [sqlTime (.getTime rs i)] 92 | (.toLocalTime sqlTime)))) 93 | 94 | ;; date processing for aggregation 95 | 96 | (defmethod driver/db-start-of-week :duckdb [_] :monday) 97 | 98 | (defmethod sql.qp/add-interval-honeysql-form :duckdb 99 | [driver hsql-form amount unit] 100 | (if (= unit :quarter) 101 | (recur driver hsql-form (* amount 3) :month) 102 | (hx/+ (hx/->timestamp hsql-form) [:raw (format "(INTERVAL '%d' %s)" (int amount) (name unit))]))) 103 | 104 | (defmethod sql.qp/date [:duckdb :default] [_ _ expr] expr) 105 | (defmethod sql.qp/date [:duckdb :minute] [_ _ expr] [:date_trunc (hx/literal :minute) expr]) 106 | (defmethod sql.qp/date [:duckdb :minute-of-hour] [_ _ expr] [:minute expr]) 107 | (defmethod sql.qp/date [:duckdb :hour] [_ _ expr] [:date_trunc (hx/literal :hour) expr]) 108 | (defmethod sql.qp/date [:duckdb :hour-of-day] [_ _ expr] [:hour expr]) 109 | (defmethod sql.qp/date [:duckdb :day] [_ _ expr] [:date_trunc (hx/literal :day) expr]) 110 | (defmethod sql.qp/date [:duckdb :day-of-month] [_ _ expr] [:day expr]) 111 | (defmethod sql.qp/date [:duckdb :day-of-year] [_ _ expr] [:dayofyear expr]) 112 | 113 | (defmethod sql.qp/date [:duckdb :day-of-week] 114 | [_ _ expr] 115 | (sql.qp/adjust-day-of-week :duckdb [:dayofweek expr])) 116 | 117 | (defmethod sql.qp/date [:duckdb :week] 118 | [_ _ expr] 119 | (sql.qp/adjust-start-of-week :duckdb (partial conj [:date_trunc] (hx/literal :week)) expr)) 120 | 121 | (defmethod sql.qp/date [:duckdb :month] [_ _ expr] [:date_trunc (hx/literal :month) expr]) 122 | (defmethod sql.qp/date [:duckdb :month-of-year] [_ _ expr] [:month expr]) 123 | (defmethod sql.qp/date [:duckdb :quarter] [_ _ expr] [:date_trunc (hx/literal :quarter) expr]) 124 | (defmethod sql.qp/date [:duckdb :quarter-of-year] [_ _ expr] [:quarter expr]) 125 | (defmethod sql.qp/date [:duckdb :year] [_ _ expr] [:date_trunc (hx/literal :year) expr]) 126 | 127 | (defmethod sql.qp/unix-timestamp->honeysql [:duckdb :seconds] 128 | [_ _ expr] 129 | [:from_unixtime expr]) 130 | 131 | ;; emty result set for queries without result (like insert...) 132 | 133 | (defn empty-rs [_] ; 134 | (reify 135 | ResultSet 136 | (getMetaData [_] 137 | (reify 138 | ResultSetMetaData 139 | (getColumnCount [_] 1) 140 | (getColumnLabel [_ _idx] "WARNING") 141 | (getColumnTypeName [_ _] "CHAR") 142 | (getColumnType [_ _] Types/CHAR))) 143 | (next [_] false) 144 | (close [_]))) 145 | 146 | ;; override native execute-statement! to make queries that does't returns ResultSet 147 | 148 | (defmethod sql-jdbc.execute/execute-statement! :sql-jdbc 149 | [_driver ^Statement stmt ^String sql] 150 | (if (.execute stmt sql) 151 | (.getResultSet stmt) 152 | (empty-rs []))) 153 | 154 | (defmethod driver/describe-database :duckdb 155 | [_ database] 156 | {:tables 157 | (with-open [conn (jdbc/get-connection (sql-jdbc.conn/db->pooled-connection-spec database))] 158 | (set 159 | (for [ 160 | {:keys [table_schema table_name]} 161 | (jdbc/query {:connection conn} 162 | ["select * from information_schema.tables"]) 163 | ] 164 | {:name table_name :schema table_schema})))}) 165 | 166 | (defmethod driver/describe-table :duckdb 167 | [_ database {table_name :name, schema :schema}] 168 | {:name table_name 169 | :schema schema 170 | :fields 171 | (with-open [conn (jdbc/get-connection (sql-jdbc.conn/db->pooled-connection-spec database))] 172 | (let [results (jdbc/query 173 | {:connection conn} 174 | [(format "select * from information_schema.columns where table_name = '%s'" table_name)])] 175 | (set 176 | (for [[idx {column_name :column_name, data_type :data_type}] (m/indexed results)] 177 | {:name column_name 178 | :database-type data_type 179 | :base-type (sql-jdbc.sync/database-type->base-type :duckdb (keyword data_type)) 180 | :database-position idx}))))}) 181 | 182 | ;; The 0.4.0 DuckDB JDBC .getImportedKeys method throws 'not implemented' yet. 183 | ;; There is no support of FK yet. 184 | (defmethod driver/describe-table-fks :duckdb 185 | [_ _ _] 186 | (set #{})) 187 | --------------------------------------------------------------------------------