├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── pull_request_template.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Makefile
├── README.md
├── docs
└── images
│ ├── config.png
│ ├── customquestion.png
│ ├── customresult.png
│ ├── datamodel.png
│ └── nativequery.png
├── environments
├── build
│ └── Dockerfile
└── test
│ ├── cubejs-token
│ ├── cubejs
│ ├── .env
│ └── schema
│ │ └── characters.js
│ ├── db
│ ├── data.csv
│ └── init.sql
│ ├── docker-compose.yml
│ └── metabase
│ └── log4j.properties
├── project.clj
├── resources
└── metabase-plugin.yaml
└── src
└── metabase
└── driver
├── cubejs.clj
└── cubejs
├── parameters.clj
├── query_processor.clj
└── utils.clj
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Cube.js version:**
27 | [e.g. v0.26.60]
28 |
29 | **Metabase version:**
30 | [e.g. v0.38.1]
31 |
32 | **Driver version:**
33 | [e.g. v0.15.0]
34 |
35 | **Additional context**
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | **Check List**
2 | - [ ] Tests has been run in packages where changes made if available
3 | - [ ] Linter has been run for changed code
4 | - [ ] Tests for the changes have been added if not covered yet
5 | - [ ] Docs have been added / updated if required
6 |
7 | **Issue Reference this PR resolves**
8 |
9 | [For example #12]
10 |
11 | **Description of Changes Made (if issue reference is not provided)**
12 |
13 | [Description goes here]
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Builds
2 | /target
3 |
4 | # Builds
5 | *.jar
6 |
7 | # Clojure dependency file
8 | *.edn
9 |
10 | # Leiningen
11 | .lein-repl-history
12 | .nrepl-port
13 |
14 | # Calva
15 | .calva
16 |
17 | # Clj-kondo (Clojure linter)
18 | .clj-kondo
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at https://github.com/pyrooka/metabase-cubejs-driver/discussions. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TESTENV?=environments/test
2 |
3 | ## stop: Stops running test environment containers
4 | .PHONY: stop
5 | stop:
6 | @cd ${TESTENV} && docker-compose stop
7 |
8 | ## start: Starts a local test environment
9 | .PHONY: start
10 | start: stop build
11 | @mkdir -p ${TESTENV}/driver
12 | @cp cubejs.metabase-driver.jar ${TESTENV}/driver
13 | @cd ${TESTENV} && docker-compose up
14 |
15 | ## up: Starts the local test environment but withouth compiling the driver (just a docker-compose up)
16 | .PHONY: up
17 | up: stop
18 | @cd ${TESTENV} && docker-compose up
19 |
20 | ## docker: Builds the docker images for the driver building and the testing
21 | .PHONY: docker
22 | docker:
23 | @echo "Building metabase-driver-builder image..."
24 | @docker build -t metabase-driver-builder environments/build
25 | @echo "Building cubejs-metabackend image..."
26 | @docker build -t cubejs-metabackend ${TESTENV}/cubejs
27 |
28 | ## build: Builds the driver
29 | .PHONY: build
30 | build:
31 | @rm -rf target cubejs.metabase-driver.jar
32 | @docker run --rm -v $(shell pwd):/driver/metabase-cubejs-driver metabase-driver-builder /bin/sh -c "lein clean; DEBUG=1 LEIN_SNAPSHOTS_IN_RELEASE=true lein uberjar"
33 | @cp target/uberjar/cubejs.metabase-driver.jar ./
34 |
35 | ## repl: Starts a local REPL server for development
36 | repl:
37 | docker run -it --rm -p 5555:5555 -v $(shell pwd):/driver/metabase-cubejs-driver metabase-driver-builder /bin/sh -c "lein repl :start :host 0.0.0.0 :port 5555"
38 |
39 | ## rmc: Remove the test docker containers.
40 | .PHONY: rmc
41 | rmc:
42 | @echo "Removing test containers..."
43 | @cd ${TESTENV} && docker-compose rm -s -f
44 |
45 | ## clean: Cleanups your workplace
46 | .PHONY: clean
47 | clean:
48 | @echo "Removing builds..."
49 | @rm -rf target
50 | @echo "Removing docker containers..."
51 | @cd ${TESTENV} && docker-compose rm -s -f
52 | @echo "Removing docker images..."
53 | @docker rmi metabase-driver-builder cubejs-metabackend
54 |
55 | .PHONY: help
56 | ## help: Prints this help message
57 | help:
58 | @echo "Usage: \n"
59 | @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NOTE
2 | CubeJS has an official SQL API now, so try that before using this driver, since it could be more reliable and well supported. You can find more info [here](https://cube.dev/docs/backend/sql).
3 |
4 | ## Update
5 | Since Cube.js now provides an official way to connect it to Metabase, there is no need for this repo anymore so I'm making it read-only. It was a great journey to learn the "basic" of Clojure, to make this project work and to see how the data flows between the 2 apps.
6 |
7 | Thanks to all the people who helped me working on this in any way!
8 |
9 |
10 | Due to an unfortunate move by someone at my previous company, the "original" repo became private and we lost all the stars, so I leave this screenshot here as a memento :) 
11 |
12 | ---
13 |
14 | # metabase-cubejs-driver
15 |
16 | [](https://img.shields.io/github/v/release/pyrooka/metabase-cubejs-driver)
17 | [](https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/master/LICENSE)
18 |
19 | Cube.js driver for Metabase. With this driver you can connect your Cube.js server to Metabase just like a DB.
20 | Metabase fetches all schemas (cubes) and that's all: you can make queries, filter the results and create beautiful charts and dashboards.
21 |
22 | Explanation:
23 |
24 | | Cube.js | Metabase |
25 | |:--------------:|:--------------:|
26 | | measure | metric & field |
27 | | dimension | field |
28 | | time dimension | field |
29 |
30 | **NOTE**: The driver is under development so expect some bugs and missing features. If you find one please create an issue.
31 |
32 | # Features
33 | ## Working
34 | - **Auto generate data model** from the schema fetched from the Cube.js API meta endpoint
35 | - **Auto create metrics** from the measures. (These metrics are "invalid" when you try to edit them but still usable in queries.)
36 | - native queries with variables
37 | - custom questions
38 | - filters, orders, limit
39 | ## Not working
40 | - Aggregations like sum, count and distinct. This must be done in Cube.js not in Metabase.
41 |
42 | # Installation
43 | ## Requirements
44 | - Metabase v0.35.0 or newer
45 | ## Get the driver
46 | ### Download
47 | Download from the [releases](https://github.com/pyrooka/metabase-cubejs-driver/releases).
48 | ### Build with Docker
49 | 1. Create the docker images: `make docker`
50 | 2. Build the driver: `make build`
51 |
52 | ### Build without Docker
53 | [Use this guide.](https://github.com/tlrobinson/metabase-http-driver/blob/master/README.md#building-the-driver)
54 |
55 | ## Copy to your Metabase plugins
56 | `cp cubejs.metabase-driver.jar /path/to/metabase/plugins/`
57 | Note: you have to restart Metabase to load new plugins
58 |
59 | # Usage
60 | 1. Add and configure your Cube.js "DB" 
61 | 2. Inspect your Data Model 
62 | 3. Create a query
63 | - Native 
64 | - Custom question 
65 | 4. Explore the data 
66 | # Development
67 | ## Roadmap
68 | [v1.0.0](https://github.com/pyrooka/metabase-cubejs-driver/milestone/1)
69 |
70 | ## Testing the driver
71 | 1. Create the docker images: `make docker`
72 | 2. Start a whole test environment: `make start`
73 |
74 | # Contributing
75 | - Any type of contributions are welcomed
76 | - If you find a bug, missing feature or a simple typo just create an issue
77 | - If you can fix/implement it create a pull request
78 |
79 | # License
80 | [GNU Affero General Public License v3.0 (AGPL)](https://github.com/pyrooka/metabase-cubejs-driver/blob/master/LICENSE)
81 |
--------------------------------------------------------------------------------
/docs/images/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/1b91147d1ca88edfe396df299cfceeaec0e98b33/docs/images/config.png
--------------------------------------------------------------------------------
/docs/images/customquestion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/1b91147d1ca88edfe396df299cfceeaec0e98b33/docs/images/customquestion.png
--------------------------------------------------------------------------------
/docs/images/customresult.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/1b91147d1ca88edfe396df299cfceeaec0e98b33/docs/images/customresult.png
--------------------------------------------------------------------------------
/docs/images/datamodel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/1b91147d1ca88edfe396df299cfceeaec0e98b33/docs/images/datamodel.png
--------------------------------------------------------------------------------
/docs/images/nativequery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrooka/metabase-cubejs-driver/1b91147d1ca88edfe396df299cfceeaec0e98b33/docs/images/nativequery.png
--------------------------------------------------------------------------------
/environments/build/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:11-jdk
2 |
3 | ADD https://raw.github.com/technomancy/leiningen/stable/bin/lein /usr/local/bin/lein
4 |
5 | RUN chmod 744 /usr/local/bin/lein && \
6 | git clone https://github.com/metabase/metabase.git --branch=release-x.37.x && \
7 | cd metabase && \
8 | lein install-for-building-drivers
9 |
10 | WORKDIR /driver/metabase-cubejs-driver
--------------------------------------------------------------------------------
/environments/test/cubejs-token:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NzM3MjQ4NzcsImV4cCI6MjQzNzcyNDg3N30.AU-bwP2oERt9IPk-bOl2WlDTg1ZmXRAoGPdtX5yIWbo
--------------------------------------------------------------------------------
/environments/test/cubejs/.env:
--------------------------------------------------------------------------------
1 | CUBEJS_DB_TYPE=postgres
2 | CUBEJS_DB_HOST=db
3 | CUBEJS_DB_NAME=postgres
4 | CUBEJS_DB_USER=postgres
5 | CUBEJS_DB_PASS=postgres
6 |
7 | CUBEJS_DEV_MODE=true
8 |
9 | DEBUG_LOG=true
10 |
11 | CUBEJS_CACHE_AND_QUEUE_DRIVER=memory
12 |
13 | CUBEJS_API_SECRET=metabase
--------------------------------------------------------------------------------
/environments/test/cubejs/schema/characters.js:
--------------------------------------------------------------------------------
1 | cube(`Characters`, {
2 | sql: `
3 | select * from characters
4 | `,
5 |
6 | joins: {
7 | },
8 |
9 | measures: {
10 |
11 | numberOfUsers: {
12 | type: `count`,
13 | description: `Number of the users`,
14 | },
15 |
16 | uniqueFirstNames: {
17 | sql: `firstname`,
18 | type: `countDistinct`,
19 | description: `Uniq first names`,
20 | },
21 | },
22 |
23 | dimensions: {
24 |
25 | countrycode: {
26 | sql: `countrycode`,
27 | type: `string`,
28 | title: `Country code`
29 | },
30 |
31 | firstname: {
32 | sql: `firstname`,
33 | type: `string`,
34 | title: `First name`,
35 | description: `First name of the character`,
36 | },
37 |
38 | lastname: {
39 | sql: `lastname`,
40 | type: `string`,
41 | title: `Last name`,
42 | description: `Last name of the character`,
43 | },
44 |
45 | active: {
46 | sql: `active`,
47 | type: `boolean`,
48 | description: `Is the user active?`
49 | },
50 |
51 | birth: {
52 | sql: `birth`,
53 | type: `time`,
54 | description: `Date of birth`
55 | }
56 | }
57 | });
--------------------------------------------------------------------------------
/environments/test/db/init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE characters(
2 | id numeric,
3 | firstname text,
4 | lastname text,
5 | countrycode char(2),
6 | birth date,
7 | active boolean);
8 |
9 | COPY characters FROM '/docker-entrypoint-initdb.d/data.csv' with (format csv, encoding 'win1252', header false, null '', quote '"');
--------------------------------------------------------------------------------
/environments/test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | image: 'postgres:alpine'
5 | environment:
6 | - POSTGRES_USER=postgres
7 | - POSTGRES_PASSWORD=postgres
8 | volumes:
9 | - ./db:/docker-entrypoint-initdb.d
10 | ports:
11 | - '5432:5432'
12 | cubejs:
13 | image: cubejs/cube:v0.25-alpine
14 | depends_on:
15 | - db
16 | ports:
17 | - '4000:4000'
18 | - '4001:3000'
19 | env_file: ./cubejs/.env
20 | volumes:
21 | - ./cubejs/schema:/cube/conf/schema
22 | metabase:
23 | image: 'metabase/metabase:v0.36.7'
24 | environment:
25 | - JAVA_OPTS=-Dlog4j.configuration=file:/log4j.properties
26 | # - MB_DB_FILE=/metabase-data/metabase.db
27 | - MB_DB_TYPE=postgres
28 | - MB_DB_DBNAME=postgres
29 | - MB_DB_PORT=5432
30 | - MB_DB_USER=postgres
31 | - MB_DB_PASS=postgres
32 | - MB_DB_HOST=db
33 | volumes:
34 | - ./metabase/log4j.properties:/log4j.properties
35 | # - ./metabase-data:/metabase-data
36 | - ./driver:/plugins
37 | depends_on:
38 | - cubejs
39 | ports:
40 | - '3000:3000'
--------------------------------------------------------------------------------
/environments/test/metabase/log4j.properties:
--------------------------------------------------------------------------------
1 | # Based on: https://github.com/metabase/metabase/blob/master/resources/log4j.properties
2 | log4j.rootLogger=WARN, console
3 |
4 | # log to the console
5 | log4j.appender.console=org.apache.log4j.ConsoleAppender
6 | log4j.appender.console.Target=System.out
7 | log4j.appender.console.layout=org.apache.log4j.PatternLayout
8 | log4j.appender.console.layout.ConversionPattern=%d{MM-dd HH:mm:ss} \u001b[1m%p %c{2}\u001b[0m :: %m%n
9 |
10 | # log to a file
11 | log4j.appender.file=org.apache.log4j.RollingFileAppender
12 | log4j.appender.file.File=${logfile.path}/metabase.log
13 | log4j.appender.file.MaxFileSize=500MB
14 | log4j.appender.file.MaxBackupIndex=2
15 | log4j.appender.file.layout=org.apache.log4j.PatternLayout
16 | log4j.appender.file.layout.ConversionPattern=%d [%t] %-5p%c - %m%n
17 |
18 | # CHANGED
19 | # Set the log level to DEBUG for the whole metabase namespace.
20 | log4j.logger.metabase=DEBUG
21 |
22 | # c3p0 connection pools tend to log useless warnings way too often; only log actual errors
23 | log4j.logger.com.mchange=ERROR
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject metabase/cubejs-driver "0.15.0"
2 | :min-lein-version "2.5.0"
3 |
4 | :profiles
5 | {:provided
6 | {:dependencies
7 | [[org.clojure/clojure "1.10.1"]
8 | [metabase-core "1.0.0-SNAPSHOT"]]}
9 |
10 | :uberjar
11 | {:auto-clean true
12 | :aot :all
13 | :omit-source true
14 | :javac-options ["-target" "1.8", "-source" "1.8"]
15 | :target-path "target/%s"
16 | :uberjar-name "cubejs.metabase-driver.jar"}})
17 |
--------------------------------------------------------------------------------
/resources/metabase-plugin.yaml:
--------------------------------------------------------------------------------
1 | # Complete list of options here: https://github.com/metabase/metabase/wiki/Metabase-Plugin-Manifest-Reference
2 | info:
3 | name: Metabase Cubejs Driver
4 | version: 0.15.0
5 | description: Cubejs REST API driver
6 | driver:
7 | name: cubejs
8 | display-name: Cube.js
9 | lazy-load: true
10 | connection-properties:
11 | - name: cubeurl
12 | display-name: URL
13 | type: string
14 | default: "http://cubejs:4000/cubejs-api/"
15 | - name: authtoken
16 | display-name: Auth token (optional)
17 | type: string
18 | default: ""
19 | - name: metrics-creator
20 | display-name: User ID for creating the metrics (optional)
21 | type: number
22 | default: 1
23 | init:
24 | - step: load-namespace
25 | namespace: metabase.driver.cubejs
26 |
--------------------------------------------------------------------------------
/src/metabase/driver/cubejs.clj:
--------------------------------------------------------------------------------
1 | (ns metabase.driver.cubejs
2 | "Cube.js REST API driver."
3 | (:require [clojure.tools.logging :as log]
4 | [medley.core :as m]
5 | [toucan.db :as db]
6 | [metabase.driver :as driver]
7 | [metabase.driver.cubejs.utils :as cube.utils]
8 | [metabase.models.metric :as metric :refer [Metric]]
9 | [metabase.driver.cubejs
10 | [query-processor :as cubejs.qp]
11 | [parameters :as parameters]]))
12 |
13 | (defn- cubejs-agg->meta-agg
14 | "Returns the name of the cubejs aggregation in metabase."
15 | [agg-name]
16 | (case agg-name
17 | "number" :count
18 | "count" :count
19 | "countDistinct" :count
20 | "countDistinctApprox" :count
21 | "sum" :count
22 | "avg" :count
23 | "min" :count
24 | "max" :count
25 | "runningTotal" :count
26 | :count))
27 |
28 | (defn- measure-in-metrics?
29 | "Checks is the given measure already in the metrics."
30 | [metrics measure-name]
31 | (some #(= (:cube-name (:definition %)) measure-name) metrics))
32 |
33 | (defn- get-cubes
34 | "Get all the cubes from the Cube.js REST API."
35 | [database]
36 | (let [resp (cube.utils/make-request "v1/meta" nil database)
37 | body (:body resp)
38 | cubes (:cubes body)]
39 | cubes))
40 |
41 | (defn- process-fields
42 | "Returns the processed fields from the 'measure' or 'dimension' block. Description must be 'measure' or 'dimension'."
43 | [fields type]
44 | (for [field fields]
45 | (merge
46 | {:name (:name field)
47 | :database-type (:type field)
48 | :field-comment (str type ": "(:description field))
49 | :description (:description field)
50 | :base-type (cube.utils/cubejs-type->base-type (keyword (:type field)))
51 | :agg-type (:aggType field)}
52 | (if (= (:type field) cube.utils/cubejs-time->metabase-time) {:special-type :type/CreationTime} nil))))
53 |
54 | ;;; ---------------------------------------------- Metabase functions ------------------------------------------------
55 |
56 | (driver/register! :cubejs)
57 |
58 | (defmethod driver/supports? [:cubejs :native-parameters] [_ _] true)
59 |
60 | (defmethod driver/supports? [:cubejs :foreign-keys] [_ _] false)
61 | (defmethod driver/supports? [:cubejs :nested-fields] [_ _] false)
62 | (defmethod driver/supports? [:cubejs :set-timezone] [_ _] false) ; ??
63 | (defmethod driver/supports? [:cubejs :basic-aggregations] [_ _] false)
64 | (defmethod driver/supports? [:cubejs :expressions] [_ _] false)
65 | (defmethod driver/supports? [:cubejs :expression-aggregations] [_ _] false)
66 | (defmethod driver/supports? [:cubejs :nested-queries] [_ _] false)
67 | (defmethod driver/supports? [:cubejs :binning] [_ _] false)
68 | (defmethod driver/supports? [:cubejs :case-sensitivity-string-filter-options] [_ _] false)
69 | (defmethod driver/supports? [:cubejs :left-join] [_ _] false)
70 | (defmethod driver/supports? [:cubejs :right-join] [_ _] false)
71 | (defmethod driver/supports? [:cubejs :inner-join] [_ _] false)
72 | (defmethod driver/supports? [:cubejs :full-join] [_ _] false)
73 |
74 | (defmethod driver/can-connect? :cubejs [_ details]
75 | (if (nil? (get-cubes {:details details})) false true))
76 |
77 | (defmethod driver/describe-database :cubejs [_ database]
78 | {:tables (set (for [cube (get-cubes database)]
79 | {:name (:name cube)
80 | :schema (:schema cube)}))})
81 |
82 | (defmethod driver/describe-table :cubejs [_ database table]
83 | (let [cubes (get-cubes database)
84 | cube (first (filter (comp (set (list (:name table))) :name) cubes))
85 | measures (process-fields (:measures cube) "measure")
86 | dimensions (process-fields (:dimensions cube) "dimension")
87 | metrics (metric/retrieve-metrics (:id table) :all)]
88 | (doseq [measure measures]
89 | (if-not (measure-in-metrics? metrics (:name measure)) ; We can use the `name` of the measure here because it is already untouched (no rename).
90 | (db/insert! Metric
91 | :table_id (:id table)
92 | :creator_id (let [creator-id (:metrics-creator (:details database))] (if (int? creator-id) creator-id (Integer/parseInt creator-id)))
93 | :name (:name measure)
94 | :description (:description measure)
95 | :definition {:source-table (:id table)
96 | :cube-name (:name measure)
97 | :aggregation [[(cubejs-agg->meta-agg (:agg-type measure))]]})))
98 | {:name (:name cube)
99 | :schema (:schema cube)
100 | ;; Segments are currently unsupported.
101 | ;; Remove the description key from the set of fields then add the `database-position`.
102 | :fields (set (for [[idx field] (m/indexed (set (concat measures dimensions)))]
103 | (assoc (dissoc field :description :agg-type) :database-position (inc idx))))}))
104 |
105 | (defmethod driver/mbql->native :cubejs [_ query]
106 | (log/debug "MBQL:" query)
107 | (cubejs.qp/mbql->cubejs query))
108 |
109 | (defmethod driver/substitute-native-parameters :cubejs
110 | [driver inner-query]
111 | (parameters/substitute-native-parameters driver inner-query))
112 |
113 | (defmethod driver/execute-reducible-query :cubejs [_ query _ respond]
114 | (log/debug "Query:" query)
115 | (cubejs.qp/execute-http-request query respond))
--------------------------------------------------------------------------------
/src/metabase/driver/cubejs/parameters.clj:
--------------------------------------------------------------------------------
1 | (ns metabase.driver.cubejs.parameters
2 | (:require [clojure
3 | [string :as str]
4 | [walk :as walk]]
5 | [clojure.tools.logging :as log]
6 | [java-time :as t]
7 | [metabase.driver.common.parameters :as params]
8 | [metabase.driver.common.parameters
9 | [dates :as date-params]
10 | [parse :as parse]
11 | [values :as values]]
12 | [metabase.query-processor
13 | [error-type :as error-type]]
14 | [metabase.util :as u]
15 | [metabase.util
16 | [date-2 :as u.date]
17 | [i18n :refer [tru]]])
18 | (:import java.time.temporal.Temporal
19 | [metabase.driver.common.parameters CommaSeparatedNumbers Date]))
20 |
21 | (defn- ->utc-instant [t]
22 | (t/instant
23 | (condp instance? t
24 | java.time.LocalDate (t/zoned-date-time t (t/local-time "00:00") (t/zone-id "UTC"))
25 | java.time.LocalDateTime (t/zoned-date-time t (t/zone-id "UTC"))
26 | t)))
27 |
28 | (defn- param-value->str
29 | [{special-type :special_type, :as field} x]
30 | (cond
31 | ;; sequences get converted to `$in`
32 | (sequential? x)
33 | (format "%s" (str/join ", " (map (partial param-value->str field) x)))
34 |
35 | ;; Date = the Parameters Date type, not an java.util.Date or java.sql.Date type
36 | ;; convert to a `Temporal` instance and recur
37 | (instance? Date x)
38 | (param-value->str field (u.date/parse (:s x)))
39 |
40 | (and (instance? Temporal x)
41 | (isa? special-type :type/UNIXTimestampSeconds))
42 | (long (/ (t/to-millis-from-epoch (->utc-instant x)) 1000))
43 |
44 | (and (instance? Temporal x)
45 | (isa? special-type :type/UNIXTimestampMilliseconds))
46 | (t/to-millis-from-epoch (->utc-instant x))
47 |
48 | ;; convert temporal types to ISODate("2019-12-09T...") (etc.)
49 | (instance? Temporal x)
50 | (format "\"%s\"" (u.date/format x))
51 |
52 | ;; there's a special record type for sequences of numbers; pull the sequence it wraps out and recur
53 | (instance? CommaSeparatedNumbers x)
54 | (param-value->str field (:numbers x))
55 |
56 | ;; for everything else, splice it in as its string representation
57 | :else
58 | (pr-str x)))
59 |
60 | (defn- field->name [field]
61 | (:name field))
62 |
63 | (defn- substitute-one-field-filter-date-range [{field :field, {param-type :type, value :value} :value}]
64 | (let [{:keys [start end]} (date-params/date-string->range value {:inclusive-end? false})
65 | start-condition (when start
66 | (format "{%s: {$gte: %s}}" (field->name field) (param-value->str field (u.date/parse start))))
67 | end-condition (when end
68 | (format "{%s: {$lt: %s}}" (field->name field) (param-value->str field (u.date/parse end))))]
69 | (if (and start-condition end-condition)
70 | (format "{$and: [%s, %s]}" start-condition end-condition)
71 | (or start-condition
72 | end-condition))))
73 |
74 | ;; Field filter value is either params/no-value (handled in `substitute-param`, a map with `:type` and `:value`, or a
75 | ;; sequence of those maps.
76 | (defn- substitute-one-field-filter [{field :field, {param-type :type, value :value} :value, :as field-filter}]
77 | ;; convert relative dates to approprate date range representations
78 | (cond
79 | (date-params/date-range-type? param-type)
80 | (substitute-one-field-filter-date-range field-filter)
81 |
82 | ;; a `date/single` like `2020-01-10`
83 | (and (date-params/date-type? param-type)
84 | (string? value))
85 | (let [t (u.date/parse value)]
86 | (format "{$and: [%s, %s]}"
87 | (format "{%s: {$gte: %s}}" (field->name field) (param-value->str field t))
88 | (format "{%s: {$lt: %s}}" (field->name field) (param-value->str field (u.date/add t :day 1)))))
89 |
90 | :else
91 | (format "%s" (param-value->str field value))))
92 |
93 | (defn- substitute-field-filter [{field :field, {:keys [value]} :value, :as field-filter}]
94 | (if (sequential? value)
95 | (format "%s" (param-value->str field value))
96 | (substitute-one-field-filter field-filter)))
97 |
98 | (defn- substitute-param [param->value [acc missing] in-optional? {:keys [k], :as param}]
99 | (let [v (get param->value k)]
100 | (cond
101 | (not (contains? param->value k))
102 | [acc (conj missing k)]
103 |
104 | (params/FieldFilter? v)
105 | (let [no-value? (= (:value v) params/no-value)]
106 | (cond
107 | ;; no-value field filters inside optional clauses are ignored and omitted entirely
108 | (and no-value? in-optional?) [acc (conj missing k)]
109 | ;; otherwise replace it with a {} which is the $match equivalent of 1 = 1, i.e. always true
110 | no-value? [(conj acc "{}") missing]
111 | :else [(conj acc (substitute-field-filter v))
112 | missing]))
113 |
114 | (= v params/no-value)
115 | [acc (conj missing k)]
116 |
117 | :else
118 | [(conj acc (param-value->str nil v)) missing])))
119 |
120 | (declare substitute*)
121 |
122 | (defn- substitute-optional [param->value [acc missing] {subclauses :args}]
123 | (let [[opt-acc opt-missing] (substitute* param->value subclauses true)]
124 | (if (seq opt-missing)
125 | [acc missing]
126 | [(into acc opt-acc) missing])))
127 |
128 | (defn- substitute*
129 | "Returns a sequence of `[[replaced...] missing-parameters]`."
130 | [param->value xs in-optional?]
131 | (reduce
132 | (fn [[acc missing] x]
133 | (cond
134 | (string? x)
135 | [(conj acc x) missing]
136 |
137 | (params/Param? x)
138 | (substitute-param param->value [acc missing] in-optional? x)
139 |
140 | (params/Optional? x)
141 | (substitute-optional param->value [acc missing] x)
142 |
143 | :else
144 | (throw (ex-info (tru "Don''t know how to substitute {0} {1}" (.getName (class x)) (pr-str x))
145 | {:type error-type/driver}))))
146 | [[] nil]
147 | xs))
148 |
149 | (defn- substitute [param->value xs]
150 | (let [[replaced missing] (substitute* param->value xs false)]
151 | (when (seq missing)
152 | (throw (ex-info (tru "Cannot run query: missing required parameters: {0}" (set missing))
153 | {:type error-type/invalid-query})))
154 | (when (seq replaced)
155 | (str/join replaced))))
156 |
157 | (defn- parse-and-substitute [param->value x]
158 | (if-not (string? x)
159 | x
160 | (u/prog1 (substitute param->value (parse/parse x))
161 | (when-not (= x <>)
162 | (log/debug (tru "Substituted {0} -> {1}" (pr-str x) (pr-str <>)))))))
163 |
164 | (defn substitute-native-parameters
165 | "Implementation of `driver/substitute-native-parameters` for Cube.js."
166 | [_ inner-query]
167 | (let [param->value (values/query->params-map inner-query)]
168 | (update inner-query :query (partial walk/postwalk (partial parse-and-substitute param->value)))))
--------------------------------------------------------------------------------
/src/metabase/driver/cubejs/query_processor.clj:
--------------------------------------------------------------------------------
1 | (ns metabase.driver.cubejs.query-processor
2 | (:require [clojure.set :as set]
3 | [clojure.string :as string]
4 | [cheshire.core :as json]
5 | [toucan.db :as db]
6 | [metabase.mbql.util :as mbql.u]
7 | [metabase.util.date-2 :as u.date]
8 | [metabase.query-processor.store :as qp.store]
9 | [metabase.models.metric :as metric :refer [Metric]]
10 | [metabase.driver.cubejs.utils :as cube.utils]
11 | [metabase.query-processor.middleware.annotate :as annotate]
12 | [java-time :as time]))
13 |
14 |
15 | (def ^:dynamic ^:private *query* nil)
16 |
17 | (def ^:private datetime-operators
18 | ["afterDate", "beforeDate", "inDateRange", "notInDateRange"])
19 |
20 | ;;; ----------------------------------------------------- common -----------------------------------------------------
21 |
22 | (defn get-metric-cube-name [metric-display-name table-id]
23 | (:cube-name (:definition (db/select-one Metric :name metric-display-name :table_id table-id :archived false))))
24 |
25 | (defn- field-description->type
26 | [field-description]
27 | (let [type (first (string/split field-description #":"))]
28 | (case type
29 | "measure" :measure
30 | "dimension" :dimension
31 | (throw (Exception. (str "Invalid field type: " (name type) ". Must be `measure` or `dimension`."))))))
32 |
33 | (defn- is-datetime-field?
34 | [[ftype & _] [vtype & _]]
35 | (or
36 | (= ftype :datetime-field)
37 | (= vtype :absolute-datetime)
38 | (= vtype :relative-datetime)))
39 |
40 | (defn- is-datetime-operator?
41 | [operator]
42 | (some #(= operator %) datetime-operators))
43 |
44 | (defn- get-older
45 | "Returns the older value."
46 | [dts1 dts2]
47 | (let [dt1 (u.date/parse dts1)
48 | dt2 (u.date/parse dts2)]
49 | (if (.isBefore dt1 dt2)
50 | dts1
51 | dts2)))
52 |
53 | (defn- get-newer
54 | "Returns the newer value."
55 | [dts1 dts2]
56 | (let [dt1 (u.date/parse dts1)
57 | dt2 (u.date/parse dts2)]
58 | (if (.isBefore dt1 dt2)
59 | dts2
60 | dts1)))
61 |
62 | (defn- lower-bound [unit t]
63 | (:start (u.date/range t unit)))
64 |
65 | (defn- upper-bound [unit t]
66 | (:end (u.date/range t unit)))
67 |
68 | (defn ^:private mbql-granularity->cubejs-granularity
69 | [granularity]
70 | (let [cubejs-granularity (granularity {:default :day
71 | :year :year
72 | :month :month
73 | :week :week
74 | :day :day
75 | :hour :hour
76 | :minute :minute
77 | :second :second
78 | :month-of-year :month
79 | :day-of-year :day
80 | :day-of-month :day
81 | :day-of-week :day})] ;; TODO :day :week-of-year :minute-of-hour, :hour-of-day Not Suported
82 | (if (nil? cubejs-granularity)
83 | (throw (Exception. (str (name granularity) " granularity not supported by Cube.js")))
84 | cubejs-granularity)))
85 |
86 | (defmulti ^:private ->rvalue
87 | "Convert something to an 'rvalue`, i.e. a value that could be used in the right-hand side of an assignment expression.
88 | (let [x 100] ...) ; x is the lvalue; 100 is the rvalue"
89 | {:arglists '([x])}
90 | mbql.u/dispatch-by-clause-name-or-class)
91 |
92 | (defmethod ->rvalue nil
93 | [_]
94 | nil)
95 |
96 | (defmethod ->rvalue Object
97 | [this]
98 | (conj [] (str this))) ; in Cube.js filter value is an array
99 |
100 | (defmethod ->rvalue :field-id
101 | [[_ field-id]]
102 | (:name (qp.store/field field-id)))
103 |
104 | (defmethod ->rvalue :aggregation-options [[_ _ ag-names]]
105 | (get-metric-cube-name (:display-name ag-names) (:source-table *query*)))
106 |
107 | (defmethod ->rvalue :aggregation [[_ value]]
108 | (if (number? value)
109 | (->rvalue (nth (:aggregation *query*) value))
110 | (->rvalue value)))
111 |
112 | (defmethod ->rvalue :aggregate-field [[_ index]]
113 | (->rvalue (nth (:aggregation *query*) index)))
114 |
115 | (defmethod ->rvalue :datetime-field
116 | [[_ field]]
117 | (->rvalue field))
118 |
119 | (defmethod ->rvalue :relative-datetime
120 | [[_ amount unit]]
121 | [(u.date/format (u.date/truncate (lower-bound unit (u.date/add unit amount)) unit))
122 | (u.date/format (u.date/truncate (upper-bound unit (u.date/add unit amount)) unit))])
123 |
124 | (defmethod ->rvalue :absolute-datetime
125 | [[_ t]]
126 | (->rvalue (u.date/format t)))
127 |
128 | (defmethod ->rvalue :value
129 | [[_ value]]
130 | (->rvalue value))
131 |
132 | ;;; ----------------------------------------------------- filter -----------------------------------------------------
133 |
134 | (defmulti ^:private parse-filter first)
135 |
136 | (defmethod parse-filter nil [] nil)
137 |
138 | ;; Metabase convert the `set` and `not set` filters to `= nil` `!= nil`.
139 | (defmethod parse-filter := [[_ field value]]
140 | (if-let [rvalue (->rvalue value)]
141 | (if (is-datetime-field? field value)
142 | {:member (->rvalue field) :operator "inDateRange" :values (concat rvalue)}
143 | {:member (->rvalue field) :operator "equals" :values rvalue})
144 | (parse-filter [:is-null field])))
145 |
146 | (defmethod parse-filter :!= [[_ field value]]
147 | (if-let [rvalue (->rvalue value)]
148 | {:member (->rvalue field) :operator "notEquals" :values rvalue}
149 | (parse-filter [:not-null field])))
150 |
151 | (defmethod parse-filter :< [[_ field value]]
152 | (if (is-datetime-field? field value)
153 | {:member (->rvalue field) :operator "beforeDate" :values (->rvalue value)}
154 | {:member (->rvalue field) :operator "lt" :values (->rvalue value)}))
155 | (defmethod parse-filter :> [[_ field value]]
156 | (if (is-datetime-field? field value)
157 | {:member (->rvalue field) :operator "afterDate" :values (->rvalue value)}
158 | {:member (->rvalue field) :operator "gt" :values (->rvalue value)}))
159 | (defmethod parse-filter :<= [[_ field value]]
160 | (if (is-datetime-field? field value)
161 | {:member (->rvalue field) :operator "beforeDate" :values (->rvalue value)}
162 | {:member (->rvalue field) :operator "lte" :values (->rvalue value)}))
163 | (defmethod parse-filter :>= [[_ field value]]
164 | (if (is-datetime-field? field value)
165 | {:member (->rvalue field) :operator "afterDate" :values (->rvalue value)}
166 | {:member (->rvalue field) :operator "gte" :values (->rvalue value)}))
167 |
168 | (defmethod parse-filter :between [[_ field min-val max-val]]
169 | ;; If the type of the fields is datetime, use inDateRange.
170 | (if (is-datetime-field? field nil)
171 | {:member (->rvalue field) :operator "inDateRange" :values (concat (->rvalue min-val) (->rvalue max-val))}
172 | [{:member (->rvalue field) :operator "gte" :values (->rvalue min-val)}
173 | {:member (->rvalue field) :operator "lte" :values (->rvalue max-val)}]))
174 |
175 | ;; Starts/ends-with not implemented in Cube.js yet.
176 | (defmethod parse-filter :starts-with [[_ _]] (throw (Exception. "\"Starts with\" filter is not supported by Cube.js yet")))
177 | (defmethod parse-filter :ends-with [[_ _]] (throw (Exception. "\"Ends with\" filter is not supported by Cube.js yet")))
178 |
179 | (defmethod parse-filter :contains [[_ field value]] {:member (->rvalue field) :operator "contains" :values (->rvalue value)})
180 | (defmethod parse-filter :does-not-contains [[_ field value]] {:member (->rvalue field) :operator "notContains" :values (->rvalue value)})
181 |
182 | (defmethod parse-filter :not-null [[_ field]] {:member (->rvalue field) :operator "set"})
183 | (defmethod parse-filter :is-null [[_ field]] {:member (->rvalue field) :operator "notSet"})
184 |
185 | (defmethod parse-filter :and [[_ & args]] (mapv parse-filter args))
186 | (defmethod parse-filter :or [[_ & args]]
187 | (let [filters (mapv parse-filter args)]
188 | (reduce (fn [result filter]
189 | (update result :values into (:values filter)))
190 | filters)))
191 |
192 | ;; Use this code if different fields can be in a single or clause, because this will check and match them.
193 | ;; NOTE: the return value is a vector so we have to handle it somehow.
194 | ;;(defmethod parse-filter :or [[_ & args]]
195 | ;; (let [filters (mapv parse-filter args)]
196 | ;; (reduce (fn [result filter]
197 | ;; (if (some #(and (= (:member %) (:member filter)) (= (:operator %) (:operator filter))) result)
198 | ;; (mapv #(if (and (= (:member %) (:member filter)) (= (:operator %) (:operator filter))) (update % :values into (:values filter))) result)
199 | ;; (conj result filter)))
200 | ;; []
201 | ;; filters)))
202 |
203 | (defmulti ^:private negate first)
204 |
205 | (defmethod negate :default [clause]
206 | (mbql.u/negate-filter-clause clause))
207 |
208 | (defmethod negate :and [[_ & subclauses]] (apply vector :and (map negate subclauses)))
209 |
210 | (defmethod negate :or [[_ & subclauses]] (apply vector :or (map negate subclauses)))
211 |
212 | (defmethod negate :contains [[_ field v opts]] [:does-not-contains field v opts])
213 |
214 | (defmethod parse-filter :not [[_ subclause]] (parse-filter (negate subclause)))
215 |
216 | (defn- datetime-filter-optimizer
217 | "Optimize the datetime filters. If we have more than one filter for a field, merge them into a single `inDateRange` filter."
218 | [result new]
219 | (if-not (some #(= (:member %) (:member new)) result)
220 | (conj result (if (<= (count (:values new)) 2)
221 | new
222 | (let [first-value (first (:values new))
223 | second-value (last (:values new))]
224 | (merge
225 | new
226 | {:values [first-value second-value]}))))
227 | (for [filter result]
228 | (if-not (= (:member filter) (:member new))
229 | filter
230 | (let [old-operator (:operator filter)
231 | old-first-value (first (:values filter))
232 | old-second-value (second (:values filter))
233 | new-operator (:operator new)
234 | new-first-value (first (:values new))
235 | new-second-value (second (:values new))]
236 | (merge
237 | filter
238 | (case old-operator
239 | "beforeDate" (case new-operator
240 | "beforeDate" {:operator "beforeDate" :values [(get-older old-first-value new-first-value)]}
241 | "afterDate" {:operator "inDateRange" :values [new-first-value, old-first-value]}
242 | "inDateRange" {:operator "inDateRange" :values [new-first-value, (get-older old-first-value new-second-value)]})
243 | "afterDate" (case new-operator
244 | "beforeDate" {:operator "inDateRange" :values [old-first-value, new-first-value]}
245 | "afterDate" {:operator "afterDate" :values [(get-newer old-first-value new-first-value)]}
246 | "inDateRange" {:operator "inDateRange" :values [(get-newer old-first-value new-first-value), new-second-value]})
247 | "inDateRange" (case new-operator
248 | "beforeDate" {:operator "inDateRange" :values [new-first-value, (get-older old-second-value new-first-value)]}
249 | "afterDate" {:operator "inDateRange" :values [(get-newer old-first-value new-first-value), old-second-value]}
250 | "inDateRange" {:operator "inDateRange" :values [(get-newer old-first-value new-first-value) (get-older old-second-value new-second-value)]}))))))))
251 |
252 | (defn- optimize-datetime-filters
253 | "If we have more than one filter to the same date field we have to transform them to a single `dateRange` filter."
254 | [filters]
255 | (let [datetime-filters (filterv #(is-datetime-operator? (:operator %)) filters)
256 | optimized-filters (reduce datetime-filter-optimizer [] datetime-filters)]
257 | optimized-filters))
258 |
259 | (defn- transform-filters
260 | "Transform the MBQL filters to Cube.js filters."
261 | [query]
262 | (let [filter (:filter query)
263 | filters (if filter (parse-filter filter) nil)
264 | raw (flatten (if (vector? filters) filters (if filters (conj [] filters) nil)))
265 | non-datetime-filters (filterv #(not (is-datetime-operator? (:operator %))) raw)
266 | datetime-filters (optimize-datetime-filters raw)
267 | result (into [] (concat non-datetime-filters (filterv #(and (is-datetime-operator? (:operator %)) (< (count (:values %)) 2)) datetime-filters)))]
268 | {:filters result}))
269 |
270 | ;;; ---------------------------------------------------- order-by ----------------------------------------------------
271 |
272 | (defn- handle-order-by
273 | [{:keys [order-by]}]
274 | ;; Iterate over the order-by fields.
275 | {:order (into {} (for [[direction field] order-by]
276 | {(->rvalue field) direction}))})
277 |
278 | ;;; ----------------------------------------------------- limit ------------------------------------------------------
279 |
280 | (defn- handle-limit
281 | [{:keys [limit]}]
282 | (if-not (nil? limit)
283 | (let [limit-limit 50000]
284 | {:limit (if (> limit limit-limit)
285 | limit-limit
286 | limit)})
287 | nil))
288 |
289 | ;;; ----------------------------------------------------- fields -----------------------------------------------------
290 |
291 | (defmulti ^:private ->cubefield
292 | "Same like the `->rvalue`, but with the value returns the field type and other optional values too."
293 | {:arglists '([x])}
294 | mbql.u/dispatch-by-clause-name-or-class)
295 |
296 | (defmethod ->cubefield nil
297 | [_]
298 | nil)
299 |
300 | (defmethod ->cubefield Object
301 | [this]
302 | this)
303 |
304 | (defmethod ->cubefield :field-id
305 | [[_ field-id]]
306 | (let [field (qp.store/field field-id)]
307 | {:name (:name field) :type (field-description->type (:description field))}))
308 |
309 | (defmethod ->cubefield :aggregation-options [[_ _ ag-names]]
310 | (if-let [metric-cube-name (get-metric-cube-name (:display-name ag-names) (:source-table *query*))]
311 | {:name metric-cube-name :type :measure}
312 | nil))
313 |
314 | (defmethod ->cubefield :datetime-field
315 | [[_ field granularity]]
316 | (let [field (->cubefield field)]
317 | {:name (:name field)
318 | :type :timeDimension
319 | :granularity (mbql-granularity->cubejs-granularity granularity)}))
320 |
321 | (defn- handle-datetime-filter
322 | [date-filter]
323 | (let [date-filters (if date-filter (parse-filter date-filter) nil)
324 | raw-filters (flatten (if (vector? date-filters) date-filters (if date-filters (conj [] date-filters) nil)))
325 | cube-date-filters (optimize-datetime-filters raw-filters)
326 | result-fields {}]
327 | (reduce (fn [result-fields, cube-date-filter]
328 | (if (< (count (:values cube-date-filter)) 2)
329 | result-fields
330 | (assoc result-fields (:member cube-date-filter) {:type :timeDimension :name (:member cube-date-filter) :dateRange (:values cube-date-filter)})))
331 | result-fields
332 | cube-date-filters)))
333 |
334 | (defn- handle-datetime-fields
335 | [time-dimensions cube-fields]
336 | (reduce (fn [time-dimensions new]
337 | (case (:type new)
338 | :timeDimension
339 | (if (contains? time-dimensions (:name new))
340 | (-> time-dimensions
341 | (assoc-in [(:name new) :granularity] (:granularity new))
342 | (assoc-in [(:name new) :type] :timeDimensionGran))
343 | (assoc time-dimensions (:name new) {:type :dimension :name (:name new) :granularity (:granularity new)}))
344 | time-dimensions))
345 | time-dimensions
346 | cube-fields))
347 |
348 |
349 | (defn- handle-measures-dimensions-fields
350 | [cube-fields]
351 | (let [result {:measures [] :dimensions [] :timeDimensions []}]
352 | (reduce (fn [result new]
353 | (case (:type new)
354 | :measure (update result :measures #(conj % (:name new)))
355 | :dimension (update result :dimensions #(conj % (:name new)))
356 | result))
357 | result
358 | cube-fields)))
359 |
360 | (defn- handle-fields
361 | [{:keys [filter fields aggregation breakout]}]
362 | (let [time-dimensions-filter (if filter (handle-datetime-filter filter) nil)
363 | fields-all (concat fields aggregation breakout)
364 | cube-fields (set (for [field fields-all] (->cubefield field)))
365 | time-dimensions (handle-datetime-fields time-dimensions-filter cube-fields)
366 | result (handle-measures-dimensions-fields cube-fields)]
367 | (reduce (fn [result new]
368 | (case (:type new)
369 | :timeDimension (update result :timeDimensions #(conj % {:dimension (:name new) :dateRange (:dateRange new)}))
370 | :timeDimensionGran (update result :timeDimensions #(conj % {:dimension (:name new) :granularity (:granularity new) :dateRange (:dateRange new)}))
371 | :dimension (update result :timeDimensions #(conj % {:dimension (:name new) :granularity (:granularity new)}))
372 | result))
373 | result
374 | (vals time-dimensions))))
375 |
376 | ;;; ----------------------------------------------- datetime granularity preprocessing ------------------------------------------------
377 |
378 | (def ^:private post-process-granularity
379 | [:month-of-year, :day-of-year, :day-of-month, :day-of-week]) ;; TODO :week-of-year :minute-of-hour, :hour-of-day Not Suported
380 |
381 | (defn- is-process-granularity?
382 | [granularity]
383 | (some #(= granularity %) post-process-granularity))
384 |
385 | (defmulti ^:private ->datetime-granularity
386 | "Same like the `->rvalue`, but with the value returns the field type and other optional values too."
387 | {:arglists '([x])}
388 | mbql.u/dispatch-by-clause-name-or-class)
389 |
390 | (defmethod ->datetime-granularity Object
391 | [this]
392 | this)
393 |
394 | (defmethod ->datetime-granularity :field-id
395 | [[_ field-id]]
396 | (let [field (qp.store/field field-id)]
397 | {:name (keyword (:name field)) :type (field-description->type (:description field))}))
398 |
399 | (defmethod ->datetime-granularity :datetime-field
400 | [[_ field granularity]]
401 | (let [field (->datetime-granularity field)]
402 | {:name (keyword (:name field))
403 | :granularity granularity}))
404 |
405 | (defn pre-datetime-granularity
406 | [{:keys [breakout]}]
407 | (let [time-breakouts (set (for [field breakout] (->datetime-granularity field)))
408 | filtered-time-breakouts (filterv #(is-process-granularity? (:granularity %)) time-breakouts)]
409 | filtered-time-breakouts))
410 | ;;; ----------------------------------------------- datetime granularity postprocessing ------------------------------------------------
411 |
412 | (defmulti ^:private extract-date (fn [granularity date] granularity))
413 |
414 |
415 | (defmethod extract-date :month-of-year [_ date]
416 | (.getValue (time/month date)))
417 |
418 | (defmethod extract-date :day-of-year [_ date]
419 | (.getValue (time/day-of-year date)))
420 |
421 | (defmethod extract-date :day-of-month [_ date]
422 | (.getValue (time/day-of-month date)))
423 |
424 | (defmethod extract-date :day-of-week [_ date]
425 | (let [java-day (.getValue (time/day-of-week date))
426 | moved-day (+ java-day 1)]
427 | (if (> moved-day 7)
428 | 1 ;; Sunday
429 | moved-day)))
430 |
431 | ; (defmethod extract-date :week-of-year [_ date]
432 | ; ) TODO :week-of-year
433 |
434 | ; (defmethod extract-date :minute-of-hour [_ date]
435 | ; ) TODO :minute-of-hour
436 |
437 | ; (defmethod extract-date :hour-of-day [_ date]
438 | ; ) TODO :hour-of-day
439 |
440 | (defn update-row-values-datetime-granularity
441 | [date-granularity-fields field-name date]
442 | (let [granularity (reduce (fn [granularity date-granularity-field]
443 | (if (= field-name (:name date-granularity-field))
444 | (:granularity date-granularity-field)
445 | granularity))
446 | {} date-granularity-fields)]
447 | (if-not (nil? granularity)
448 | (extract-date granularity (time/local-date-time date))
449 | date)))
450 |
451 | ;;; -------------------------------------------------- query build ---------------------------------------------------
452 |
453 | (defn mbql->cubejs
454 | "Build a valid Cube.js query from the generated MBQL."
455 | [base-query]
456 | (binding [*query* (:query base-query)]
457 | (let [query (:query base-query)
458 | fields (handle-fields query)
459 | filters (transform-filters query)
460 | order-by (handle-order-by query)
461 | limit (handle-limit query)
462 | native-query (merge fields filters order-by limit)]
463 | {:query (json/generate-string native-query {:pretty true})
464 | :measure-aliases (into {} (for [[_ _ names] (:aggregation query)] {(keyword (get-metric-cube-name (:display-name names) (:source-table query))) (keyword (:name names))}))
465 | :date-granularity-fields (pre-datetime-granularity query)})))
466 |
467 | ;;; ----------------------------------------------- result processing ------------------------------------------------
468 |
469 | (defn- parse-number
470 | "From: https://github.com/metabase/metabase/blob/master/src/metabase/query_processor/middleware/parameters/mbql.clj#L14"
471 | [value]
472 | (cond
473 | (not (string? value)) value
474 | ;; if the string contains a period then convert to a Double
475 | (re-find #"\." value)
476 | (Double/parseDouble value)
477 |
478 | ;; otherwise convert to a Long
479 | :else
480 | (Long/parseLong value)))
481 |
482 | (defn- get-types
483 | "Extract the types for each field in the response from the annotation block."
484 | [annotation]
485 | (into {}
486 | (for [fields (vals annotation)]
487 | (into {}
488 | (for [[name info] fields]
489 | {name ((keyword (:type info)) cube.utils/cubejs-type->base-type)})))))
490 |
491 | (defn- update-row-values
492 | [row num-cols date-granularity-cols]
493 | (reduce-kv
494 | (fn [row key val]
495 | (let [num-val (if (some #(= key %) num-cols) (parse-number val) val)
496 | result-val (if (some #(= key (:name %)) date-granularity-cols) (update-row-values-datetime-granularity date-granularity-cols key num-val) num-val)]
497 | (assoc row key result-val)))
498 | {} row))
499 |
500 | (defn- convert-values
501 | "Convert the values in the rows to the correct type."
502 | [rows types date-granularity-cols]
503 | ;; Get the number fields from the types.
504 | (let [num-cols (map first (filter #(= (second %) :type/Number) types))]
505 | (map #(update-row-values % num-cols date-granularity-cols) rows)))
506 |
507 | (defn execute-http-request [query respond]
508 | (let [native (if (:native query) (:native query) (mbql->cubejs query)) ; If no native query in the query let's generate one (e.g. "View the SQL").
509 | native-query (:query native)
510 | resp (cube.utils/make-request "v1/load" native-query nil)
511 | rows (:data (:body resp))
512 | annotation (:annotation (:body resp))
513 | types (get-types annotation)
514 | aliases (:measure-aliases native)
515 | cols (vec (for [name (keys (first rows))] {:name (mbql.u/qualified-name (if-let [orig-name ((keyword name) aliases)] orig-name name))}))
516 | rows (convert-values rows types (:date-granularity-fields native))
517 | cols-info (if (= (:type query) :native) cols (annotate/merged-column-info query {:cols cols}))
518 | cols-name (map #(keyword (:name %)) cols-info)
519 | reverse-aliases (set/map-invert aliases)
520 | cols-name-cube (for [col-name cols-name] (if-let [cube-name (col-name reverse-aliases)] cube-name col-name))
521 | result (for [row rows] ((apply juxt cols-name-cube) row))]
522 | (respond
523 | {:cols cols}
524 | result)))
--------------------------------------------------------------------------------
/src/metabase/driver/cubejs/utils.clj:
--------------------------------------------------------------------------------
1 | (ns metabase.driver.cubejs.utils
2 | (:require [clojure.tools.logging :as log]
3 | [clj-http.client :as client]
4 | [metabase.query-processor.store :as qp.store]))
5 |
6 |
7 | ;; Is there a better type? https://github.com/metabase/metabase/blob/master/src/metabase/types.clj#L81
8 | (def cubejs-time->metabase-time
9 | :type/DateTime)
10 |
11 | (def cubejs-type->base-type
12 | {:string :type/Text
13 | :number :type/Number
14 | :boolean :type/Boolean
15 | :time cubejs-time->metabase-time})
16 |
17 | (defn- get-cube-api-url
18 | "Returns the Cube.js API URL from the config."
19 | []
20 | (:cubeurl (:details (qp.store/database))))
21 |
22 | (defn- check-url-ending-slash
23 | "If the last character of the URL is not a '/' append one."
24 | [url]
25 | (if-not (= (last url) \/) (str url "/") url))
26 |
27 | (defn- get-cube-auth-token
28 | "Returns the authentication token for the Cube.js API."
29 | []
30 | (:authtoken (:details (qp.store/database))))
31 |
32 | (defn make-request
33 | "Make the HTTP request to the Cube.js API and return the response.
34 | If the response is 200 with a 'Continue wait' error message, try again."
35 | [resource query database]
36 | (let [api-url (if (nil? database) (get-cube-api-url) (:cubeurl (:details database)))
37 | url (str (check-url-ending-slash api-url) resource)
38 | auth-token (if (nil? database) (get-cube-auth-token) (:authtoken (:details database)))
39 | spanId (.toString (java.util.UUID/randomUUID))]
40 | (loop [requestSequenceId 1]
41 | (log/debug "Request:" url auth-token query spanId requestSequenceId)
42 | (let [resp (client/request {:method :get
43 | :url url
44 | :headers {:authorization auth-token
45 | :x-request-id (str spanId '-span- requestSequenceId)}
46 | :query-params {"query" query}
47 | :accept :json
48 | :as :json
49 | :throw-exceptions false})
50 | status-code (:status resp)
51 | body (:body resp)]
52 | (case status-code
53 | 200 (if (= (:error body) "Continue wait") ; check does the response contains "Continue wait" message or not.
54 | (do
55 | (Thread/sleep 2000) ; If it contains, wait 2 sec,
56 | (recur (+ requestSequenceId 1))) ; then retry the query.
57 | resp)
58 | 400 (throw (Exception. (format "Error occured: %s" body)))
59 | 403 (throw (Exception. "Authorization error. Check your auth token!"))
60 | 500 (throw (Exception. (format "Internal server error: %s" body)))
61 | :else (throw (Exception. (format "Unknown error. Body: %s" body))))))))
--------------------------------------------------------------------------------