diff --git a/lib/assets/connection_cell/main.js b/lib/assets/connection_cell/main.js index 56744c1..ecdae20 100644 --- a/lib/assets/connection_cell/main.js +++ b/lib/assets/connection_cell/main.js @@ -802,6 +802,83 @@ export function init(ctx, info) { `, }; + const ClickhouseForm = { + name: "ClickhouseForm", + + components: { + BaseInput: BaseInput, + BaseSwitch: BaseSwitch, + BaseSecret: BaseSecret, + BaseSelect: BaseSelect, + }, + + props: { + fields: { + type: Object, + default: {}, + }, + }, + + template: ` +
+ + + +
+
+ + + +
+ `, + }; + const app = Vue.createApp({ components: { BaseInput: BaseInput, @@ -813,6 +890,7 @@ export function init(ctx, info) { SQLServerForm: SQLServerForm, BigQueryForm: BigQueryForm, AthenaForm: AthenaForm, + ClickhouseForm: ClickhouseForm, }, template: ` @@ -847,6 +925,7 @@ export function init(ctx, info) { + @@ -870,6 +949,7 @@ export function init(ctx, info) { { label: "Google BigQuery", value: "bigquery" }, { label: "AWS Athena", value: "athena" }, { label: "Snowflake", value: "snowflake" }, + { label: "Clickhouse", value: "clickhouse" }, { label: "SQL Server", value: "sqlserver" }, ], }; @@ -888,6 +968,10 @@ export function init(ctx, info) { return this.fields.type === "snowflake"; }, + isClickhouse() { + return this.fields.type === "clickhouse"; + }, + isBigQuery() { return this.fields.type === "bigquery"; }, diff --git a/lib/assets/sql_cell/main.js b/lib/assets/sql_cell/main.js index 087397a..d5ee136 100644 --- a/lib/assets/sql_cell/main.js +++ b/lib/assets/sql_cell/main.js @@ -263,7 +263,8 @@ export function init(ctx, payload) { athena: "AWS Athena", snowflake: "Snowflake", sqlserver: "SQL Server", - duckdb: "DuckDB" + duckdb: "DuckDB", + clickhouse: "Clickhouse" }, }; }, diff --git a/lib/kino_db/connection_cell.ex b/lib/kino_db/connection_cell.ex index 64ea55c..e4acd69 100644 --- a/lib/kino_db/connection_cell.ex +++ b/lib/kino_db/connection_cell.ex @@ -149,6 +149,9 @@ defmodule KinoDB.ConnectionCell do else: ~w|database hostname port use_ipv6 username password use_ssl cacertfile instance| + "clickhouse" -> + ~w|scheme username password_secret hostname port database| + type when type in ["postgres", "mysql"] -> if fields["use_password_secret"], do: ~w|database hostname port use_ipv6 use_ssl cacertfile username password_secret|, @@ -189,6 +192,9 @@ defmodule KinoDB.ConnectionCell do "sqlserver" -> ~w|hostname port| + "clickhouse" -> + ~w|hostname port| + type when type in ["postgres", "mysql"] -> ~w|hostname port| end @@ -323,6 +329,14 @@ defmodule KinoDB.ConnectionCell do end end + defp to_quoted(%{"type" => "clickhouse"} = attrs) do + quote do + opts = unquote(trim_opts(shared_options(attrs) ++ clickhouse_options(attrs))) + + {:ok, unquote(quoted_var(attrs["variable"]))} = Kino.start_child({Ch, opts}) + end + end + defp quoted_access_key(%{"secret_access_key" => password}), do: password defp quoted_access_key(%{"secret_access_key_secret" => ""}), do: "" @@ -423,6 +437,12 @@ defmodule KinoDB.ConnectionCell do end end + defp clickhouse_options(attrs) do + [ + scheme: attrs["scheme"] || "http" + ] + end + defp quoted_var(string), do: {String.to_atom(string), [], nil} defp quoted_pass(%{"password" => password}), do: password @@ -497,6 +517,12 @@ defmodule KinoDB.ConnectionCell do end end + defp missing_dep(%{"type" => "clickhouse"}) do + unless Code.ensure_loaded?(Ch) do + ~s|{:ch, "~> 0.2"}| + end + end + defp missing_dep(_ctx), do: nil defp join_quoted(quoted_blocks) do diff --git a/lib/kino_db/sql_cell.ex b/lib/kino_db/sql_cell.ex index 89ee545..349dc2b 100644 --- a/lib/kino_db/sql_cell.ex +++ b/lib/kino_db/sql_cell.ex @@ -219,6 +219,10 @@ defmodule KinoDB.SQLCell do to_quoted(attrs, quote(do: Tds), fn n -> "@#{n}" end) end + defp to_quoted(%{"connection" => %{"type" => "clickhouse"}} = attrs) do + to_quoted(attrs, quote(do: Ch), fn n -> "{$#{n}:String}" end) + end + # Explorer-based defp to_quoted(%{"connection" => %{"type" => "snowflake"}} = attrs) do to_explorer_quoted(attrs, fn n -> "?#{n}" end) diff --git a/test/kino_db/connection_cell_test.exs b/test/kino_db/connection_cell_test.exs index 2bbc4d3..0db2c0b 100644 --- a/test/kino_db/connection_cell_test.exs +++ b/test/kino_db/connection_cell_test.exs @@ -207,6 +207,19 @@ defmodule KinoDB.ConnectionCellTest do {:ok, db} = Kino.start_child({Adbc.Database, driver: :snowflake, uri: uri}) {:ok, conn} = Kino.start_child({Adbc.Connection, database: db})\ ''' + + assert ConnectionCell.to_source(put_in(attrs["type"], "clickhouse")) == ~s''' + opts = [ + hostname: "localhost", + port: 4444, + username: "admin", + password: "pass", + database: "default", + scheme: "http" + ] + + {:ok, conn} = Kino.start_child({Ch, opts})\ + ''' end test "generates empty source code when required fields are missing" do @@ -216,6 +229,7 @@ defmodule KinoDB.ConnectionCellTest do assert ConnectionCell.to_source(put_in(@empty_required_fields["type"], "bigquery")) == "" assert ConnectionCell.to_source(put_in(@empty_required_fields["type"], "athena")) == "" assert ConnectionCell.to_source(put_in(@empty_required_fields["type"], "snowflake")) == "" + assert ConnectionCell.to_source(put_in(@empty_required_fields["type"], "clickhouse")) == "" end test "generates empty source code when all conditional fields are missing" do diff --git a/test/kino_db/sql_cell_test.exs b/test/kino_db/sql_cell_test.exs index 4ede322..4044f38 100644 --- a/test/kino_db/sql_cell_test.exs +++ b/test/kino_db/sql_cell_test.exs @@ -122,6 +122,10 @@ defmodule KinoDB.SQLCellTest do result = Explorer.DataFrame.from_query!(conn, ~S"SELECT id FROM users", [])\ """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == """ + result = Ch.query!(conn, ~S"SELECT id FROM users", [])\ + """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "sqlserver")) == """ result = Tds.query!(conn, ~S"SELECT id FROM users", [])\ """ @@ -206,6 +210,18 @@ defmodule KinoDB.SQLCellTest do )\ ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == ~s''' + result = + Ch.query!( + conn, + ~S""" + SELECT id FROM users + WHERE last_name = 'Sherlock' + """, + [] + )\ + ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "sqlserver")) == ~s''' result = Tds.query!( @@ -277,6 +293,15 @@ defmodule KinoDB.SQLCellTest do )\ ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == ~s''' + result = + Ch.query!( + conn, + ~S"SELECT id FROM users WHERE id {$1:String} AND name LIKE {$2:String}", + [user_id, search <> \"%\"] + )\ + ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "sqlserver")) == ~s''' result = Tds.query!(conn, ~S"SELECT id FROM users WHERE id @1 AND name LIKE @2", [ @@ -375,6 +400,19 @@ defmodule KinoDB.SQLCellTest do )\ ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == ~s''' + result = + Ch.query!( + conn, + ~S""" + SELECT id from users + -- WHERE id = {{user_id1}} + /* WHERE id = {{user_id2}} */ WHERE id = {$1:String} + """, + [user_id3] + )\ + ''' + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "sqlserver")) == ~s''' result = Tds.query!( @@ -422,6 +460,10 @@ defmodule KinoDB.SQLCellTest do result = Explorer.DataFrame.from_query!(conn, ~S"SELECT id FROM users", [])\ """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == """ + result = Ch.query!(conn, ~S"SELECT id FROM users", [])\ + """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "sqlserver")) == """ result = Tds.query!(conn, ~S"SELECT id FROM users", [], timeout: 30000)\ """ @@ -452,6 +494,10 @@ defmodule KinoDB.SQLCellTest do result = DF.from_query!(conn, ~S"SELECT id FROM users", [])\ """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "clickhouse")) == """ + result = Ch.query!(conn, ~S"SELECT id FROM users", [])\ + """ + assert SQLCell.to_source(put_in(attrs["connection"]["type"], "bigquery")) == """ result = Req.post!(conn, bigquery: {~S"SELECT id FROM users", []}).body\ """