@@ -3,117 +3,156 @@ defmodule ReqCH do
33 A Req plugin for ClickHouse.
44
55 By default, `ReqCH` will use TSV as the default output format.
6- To change that, see the `attach /2` docs for details.
6+ To change that, see the `new /2` docs for details.
77 """
88
9- @ connection_options [
10- :database
11- ]
12-
13- @ general_options [
14- :clickhouse ,
9+ @ options [
10+ :database ,
1511 :format
1612 ]
1713
1814 @ formats_page "https://clickhouse.com/docs/en/interfaces/formats"
1915 @ supported_formats ~w( TabSeparated TabSeparatedRaw TabSeparatedWithNames TabSeparatedWithNamesAndTypes TabSeparatedRawWithNames TabSeparatedRawWithNamesAndTypes Template TemplateIgnoreSpaces CSV CSVWithNames CSVWithNamesAndTypes CustomSeparated CustomSeparatedWithNames CustomSeparatedWithNamesAndTypes SQLInsert Values Vertical JSON JSONAsString JSONAsObject JSONStrings JSONColumns JSONColumnsWithMetadata JSONCompact JSONCompactStrings JSONCompactColumns JSONEachRow PrettyJSONEachRow JSONEachRowWithProgress JSONStringsEachRow JSONStringsEachRowWithProgress JSONCompactEachRow JSONCompactEachRowWithNames JSONCompactEachRowWithNamesAndTypes JSONCompactStringsEachRow JSONCompactStringsEachRowWithNames JSONCompactStringsEachRowWithNamesAndTypes JSONObjectEachRow BSONEachRow TSKV Pretty PrettyNoEscapes PrettyMonoBlock PrettyNoEscapesMonoBlock PrettyCompact PrettyCompactNoEscapes PrettyCompactMonoBlock PrettyCompactNoEscapesMonoBlock PrettySpace PrettySpaceNoEscapes PrettySpaceMonoBlock PrettySpaceNoEscapesMonoBlock Prometheus Protobuf ProtobufSingle ProtobufList Avro AvroConfluent Parquet ParquetMetadata Arrow ArrowStream ORC One Npy RowBinary RowBinaryWithNames RowBinaryWithNamesAndTypes RowBinaryWithDefaults Native Null XML CapnProto LineAsString Regexp RawBLOB MsgPack MySQLDump DWARF Markdown Form)
2016
2117 @ doc """
22- Attach this plugin to a Req Request .
18+ Builds a new `%Req.Request{}` for ClickHouse requests .
2319
2420 ## Options
2521
26- * `:clickhouse` - The query to be performed. If not provided,
27- the request and response pipelines won't be modified.
22+ This function can receive any option that `Req.new/1` accepts,
23+ plus the ones described below.
24+
25+ To set a new endpoint, use the `:base_url` option from `Req`.
26+ It is by default "http://localhost:8123".
27+
2828 * `:format` - Optional. The format of the response. Default is `:tsv`.
2929 This option accepts `:tsv`, `:csv`, `:json` or `:explorer` as atoms.
3030
3131 It also accepts all formats described in the #{ @ formats_page } page.
3232
3333 The `:explorer` format is special, and will build an Explorer dataframe
3434 in case the `:explorer` dependency is installed.
35+
3536 * `:database` - Optional. The database to use in the queries.
3637 Default is `nil`.
3738
3839 ## Examples
3940
41+ After setting a default database, one can make a request directly:
42+
43+ iex> req = ReqCH.new(database: "system")
44+ iex> Req.post!(req, body: "SELECT number FROM numbers LIMIT 3").body
45+ 1\n 2\n 3\n
46+
47+ It's also possible to make a query using `Req.get/2`:
48+
49+ iex> req = ReqCH.new(database: "system")
50+ iex> Req.get!(req, params: [query: "SELECT number FROM numbers LIMIT 3"]).body
51+ 1\n 2\n 3\n
52+
53+ In case the server needs authentication, it's possible to use `Req` options for that.
54+
55+ iex> req = ReqCH.new(base_url: "http://example.org:8123", auth: {:basic, "user:pass"})
56+ iex> Req.post!(req, body: "SELECT number FROM system.numbers LIMIT 3").body
57+ "0\\ n1\\ n2\\ n"
58+
59+ """
60+ @ spec new ( Keyword . t ( ) ) :: Req.Request . t ( )
61+ def new ( opts \\ [ ] ) do
62+ attach ( Req . new ( base_url: "http://localhost:8123" ) , opts )
63+ end
64+
65+ defp attach ( % Req.Request { } = req , opts ) do
66+ req
67+ |> Req.Request . prepend_request_steps ( clickhouse_run: & run / 1 )
68+ |> Req.Request . register_options ( @ options )
69+ |> Req.Request . merge_options ( opts )
70+ end
71+
72+ defguardp is_query_params ( value ) when is_list ( value ) or is_map ( value )
73+
74+ @ doc """
75+ Performs a query against ClickHouse API.
76+
77+ See docs from `new/1` for details about the options.
78+
79+ ## Examples
80+
4081 Queries can be performed using both `Req.get/2` or `Req.post/2`, but GET
4182 is "read-only" and commands like `CREATE` or `INSERT` cannot be used with it.
83+ For that reason, by default we perform a `POST` request.
84+ To change that, use `query/2` with a pre-configured `req`.
4285
4386 A plain query:
4487
45- iex> req = Req.new() |> ReqCH.attach( )
46- iex> Req.get!(req, clickhouse: "SELECT number FROM system.numbers LIMIT 3") .body
88+ iex> {:ok, response} = ReqCH.query("SELECT number FROM system.numbers LIMIT 3" )
89+ iex> response .body
4790 "0\\ n1\\ n2\\ n"
4891
4992 Changing the format to `:explorer` will return a dataframe:
5093
51- iex> req = Req.new() |> ReqCH.attach( )
52- iex> Req.get!(req, clickhouse: "SELECT number FROM system.numbers LIMIT 3", format: :explorer) .body
94+ iex> {:ok, response} = ReqCH.query("SELECT number FROM system.numbers LIMIT 3", [], format: :explorer )
95+ iex> response .body
5396 #Explorer.DataFrame<
5497 Polars[3 x 1]
5598 number u64 [0, 1, 2]
5699 >
57100
58101 Using parameters is also possible:
59102
60- iex> req = Req.new() |> ReqCH.attach( format: :explorer, database: "system")
61- iex> Req.get!(req, clickhouse: { "SELECT number FROM numbers WHERE number > {num:UInt8} LIMIT 3", [num: 5]}).body
103+ iex> opts = [ format: :explorer, database: "system"]
104+ iex> {:ok, response} = ReqCH.query( "SELECT number FROM numbers WHERE number > {num:UInt8} LIMIT 3", [num: 5], opts)
62105 #Explorer.DataFrame<
63106 Polars[3 x 1]
64107 number u64 [6, 7, 8]
65108 >
66109
67- In case a different endpoint is needed, we can use the `:base_url` option
68- from `Req` to set that value. We can use use `:auth` to pass down the username
69- and password:
110+ This function can accept `Req` options, as well as mixing them with `ReqCH` options:
70111
71- iex> req = Req.new( base_url: "https ://example.org:8123", auth: {:basic, "user:pass"})
72- iex> req = ReqCH.attach(req )
73- iex> Req.post!(req, clickhouse: "SELECT number FROM system.numbers LIMIT 3") .body
112+ iex> opts = [ base_url: "http ://example.org:8123", database: "system", auth: {:basic, "user:pass"}]
113+ iex> {:ok, response} = ReqCH.query("SELECT number FROM numbers LIMIT 3", [], opts )
114+ iex> response .body
74115 "0\\ n1\\ n2\\ n"
75116
76117 """
77- def attach ( % Req.Request { } = request , opts \\ [ ] ) do
78- request
79- |> Req.Request . prepend_request_steps ( clickhouse_run: & run / 1 )
80- |> Req.Request . register_options ( @ connection_options ++ @ general_options )
81- |> Req.Request . merge_options ( opts )
118+ @ spec query ( sql_query :: binary ( ) , params :: Map . t ( ) | Keyword . t ( ) , opts :: Keyword . t ( ) ) ::
119+ { :ok , Req.Response . t ( ) } | { :error , binary ( ) }
120+ def query ( sql_query , params \\ [ ] , opts \\ [ ] )
121+
122+ def query ( sql_query , params , opts )
123+ when is_binary ( sql_query ) and is_query_params ( params ) and is_list ( opts ) do
124+ opts
125+ |> new ( )
126+ |> put_params ( prepare_params ( params ) )
127+ |> Req . post ( body: sql_query )
82128 end
83129
84- defp run ( % Req.Request { private: % { clickhouse_format: _ } } = request ) , do: request
85-
86- defp run ( % Req.Request { options: % { clickhouse: _query } } = request ) do
87- request = update_in ( request . options , & Map . put_new ( & 1 , :base_url , "http://localhost:8123" ) )
88-
89- request
90- |> add_format ( )
91- |> add_query ( )
92- |> maybe_add_database ( )
93- |> Req.Request . append_response_steps ( clickhouse_result: & handle_clickhouse_result / 1 )
130+ @ doc """
131+ Same as `query/3`, but raises in case of error.
132+ """
133+ @ spec query! ( sql_query :: binary ( ) , params :: Map . t ( ) | Keyword . t ( ) , opts :: Keyword . t ( ) ) ::
134+ Req.Response . t ( )
135+ def query! ( sql_query , params \\ [ ] , opts \\ [ ] )
136+
137+ def query! ( sql_query , params , opts )
138+ when is_binary ( sql_query ) and is_query_params ( params ) and is_list ( opts ) do
139+ case query ( sql_query , params , opts ) do
140+ { :ok , response } -> response
141+ { :error , exception } -> raise exception
142+ end
94143 end
95144
96- defp run ( % Req.Request { } = request ) , do: request
97-
98- defp add_query ( % Req.Request { } = request ) do
99- query = Req.Request . fetch_option! ( request , :clickhouse )
145+ defp run ( % Req.Request { private: % { clickhouse_format: _ } } = request ) , do: request
100146
101- case query do
102- { sql , params } when is_binary ( sql ) and ( is_list ( params ) or is_map ( params ) ) ->
103- request
104- |> add_sql_part ( sql )
105- |> put_params ( prepare_params ( params ) )
147+ defp run ( % Req.Request { } = request ) do
148+ request = update_in ( request . options , & Map . put_new ( & 1 , :base_url , "http://localhost:8123" ) )
106149
107- sql when is_binary ( sql ) ->
108- add_sql_part ( request , sql )
150+ with % Req.Request { } = req1 <- add_format ( request ) ,
151+ % Req.Request { } = req2 <- maybe_add_database ( req1 ) do
152+ Req.Request . append_response_steps ( req2 , clickhouse_result: & handle_clickhouse_result / 1 )
109153 end
110154 end
111155
112- defp add_sql_part ( % Req.Request { method: :post } = request , sql ) , do: % { request | body: sql }
113-
114- defp add_sql_part ( % Req.Request { method: :get } = request , sql ) ,
115- do: put_params ( request , query: sql )
116-
117156 defp put_params ( request , params ) do
118157 encoded = URI . encode_query ( params , :rfc3986 )
119158
@@ -127,14 +166,23 @@ defmodule ReqCH do
127166 Enum . map ( params , fn { key , value } -> { "param_#{ key } " , value } end )
128167 end
129168
169+ @ valid_formats [ :tsv , :csv , :json , :explorer ]
170+
130171 defp add_format ( % Req.Request { } = request ) do
131- format = request |> Req.Request . get_option ( :format , :tsv ) |> normalise_format! ( )
172+ format_option = Req.Request . get_option ( request , :format , :tsv )
173+ format = normalise_format ( format_option )
132174
133- format_header = with :explorer <- format , do: "Parquet"
175+ if format do
176+ format_header = with :explorer <- format , do: "Parquet"
134177
135- request
136- |> Req.Request . put_private ( :clickhouse_format , format )
137- |> Req.Request . put_header ( "x-clickhouse-format" , format_header )
178+ request
179+ |> Req.Request . put_private ( :clickhouse_format , format )
180+ |> Req.Request . put_header ( "x-clickhouse-format" , format_header )
181+ else
182+ raise ArgumentError ,
183+ "the given format #{ inspect ( format_option ) } is invalid. Expecting one of #{ inspect ( @ valid_formats ) } " <>
184+ "or one of the valid options described in #{ @ formats_page } "
185+ end
138186 end
139187
140188 defp normalise_format ( :tsv ) , do: "TabSeparated"
@@ -154,17 +202,6 @@ defmodule ReqCH do
154202
155203 defp normalise_format ( _ ) , do: nil
156204
157- @ valid_formats [ :tsv , :csv , :json , :explorer ]
158- defp normalise_format! ( format ) do
159- if valid = normalise_format ( format ) do
160- valid
161- else
162- raise ArgumentError ,
163- "the given format #{ inspect ( format ) } is invalid. Expecting one of #{ inspect ( @ valid_formats ) } " <>
164- "or one of the valid options described in #{ @ formats_page } "
165- end
166- end
167-
168205 defp maybe_add_database ( % Req.Request { } = request ) do
169206 if database = Req.Request . get_option ( request , :database ) do
170207 put_params ( request , database: database )
0 commit comments