Skip to content

Commit 5e1b270

Browse files
committed
feat: expose scan function
1 parent 64bddd5 commit 5e1b270

File tree

5 files changed

+112
-2
lines changed

5 files changed

+112
-2
lines changed

c_src/libpg_query_ex.c

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,62 @@ static ERL_NIF_TERM deparse_query(ErlNifEnv *env, int argc,
113113
}
114114
}
115115

116+
static ERL_NIF_TERM scan_query(ErlNifEnv *env, int argc,
117+
const ERL_NIF_TERM argv[]) {
118+
ErlNifBinary query;
119+
ERL_NIF_TERM term;
120+
121+
if (argc == 1 && enif_inspect_binary(env, argv[0], &query)) {
122+
// add one more byte for the null termination
123+
char statement[query.size + 1];
124+
125+
strncpy(statement, (char *)query.data, query.size);
126+
127+
// terminate the string
128+
statement[query.size] = 0;
129+
130+
PgQueryScanResult result = pg_query_scan(statement);
131+
132+
if (result.error) {
133+
ERL_NIF_TERM error_map = enif_make_new_map(env);
134+
135+
if (!enif_make_map_put(
136+
env,
137+
error_map,
138+
enif_make_atom(env, "message"),
139+
make_binary(env, result.error->message),
140+
&error_map
141+
)) {
142+
return enif_raise_exception(env, make_binary(env, "failed to update map"));
143+
}
144+
145+
if (result.error->cursorpos > 0 && !enif_make_map_put(
146+
env,
147+
error_map,
148+
enif_make_atom(env, "cursorpos"),
149+
// drop the cursorpos by one, so it's zero-indexed
150+
enif_make_int(env, result.error->cursorpos - 1),
151+
&error_map
152+
)) {
153+
return enif_raise_exception(env, make_binary(env, "failed to update map"));
154+
}
155+
156+
term = enif_make_tuple2(env, enif_make_atom(env, "error"), error_map);
157+
} else {
158+
term = result_tuple(env, "ok", result.pbuf.data, result.pbuf.len);
159+
}
160+
pg_query_free_scan_result(result);
161+
162+
return term;
163+
} else {
164+
return enif_make_badarg(env);
165+
}
166+
}
167+
116168
static ErlNifFunc funcs[] = {
117169
{"parse_query", 1, parse_query},
118-
{"deparse_query", 1, deparse_query}
170+
{"deparse_query", 1, deparse_query},
171+
{"scan_query", 1, scan_query}
119172
};
120173

121174
ERL_NIF_INIT(Elixir.PgQuery.Parser, funcs, NULL, NULL, NULL, NULL)

lib/pg_query.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,18 @@ defmodule PgQuery do
2525
"""
2626
@spec protobuf_to_query!(%PgQuery.ParseResult{}) :: String.t() | no_return()
2727
defdelegate protobuf_to_query!(parse_result), to: PgQuery.Parser
28+
29+
@doc """
30+
Scans the binary statement `stmt` into tokens.
31+
"""
32+
@spec scan(String.t()) :: {:ok, %PgQuery.ScanResult{}} | {:error, error()}
33+
defdelegate scan(stmt), to: PgQuery.Parser
34+
35+
@doc """
36+
Scans the binary statement `stmt` into tokens.
37+
38+
Raises if the statement is invalid.
39+
"""
40+
@spec scan!(String.t()) :: %PgQuery.ScanResult{} | no_return()
41+
defdelegate scan!(stmt), to: PgQuery.Parser
2842
end

lib/pg_query/parser.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ defmodule PgQuery.Parser do
4848
end
4949
end
5050

51+
def scan(query) when is_binary(query) do
52+
with {:ok, proto} <- scan_query(query) do
53+
Protox.decode(proto, PgQuery.ScanResult)
54+
end
55+
end
56+
57+
def scan!(query) when is_binary(query) do
58+
case scan_query(query) do
59+
{:ok, proto} ->
60+
Protox.decode!(proto, PgQuery.ScanResult)
61+
62+
{:error, %{message: message}} ->
63+
raise Error, message: message
64+
end
65+
end
66+
5167
def parse_query(_query), do: :erlang.nif_error(:nif_not_loaded)
5268
def deparse_query(_encoded_proto), do: :erlang.nif_error(:nif_not_loaded)
69+
def scan_query(_query), do: :erlang.nif_error(:nif_not_loaded)
5370
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule PgQuery.MixProject do
55
[
66
app: :pg_query_ex,
77
elixir: "~> 1.13",
8-
version: "0.6.0",
8+
version: "0.6.1",
99
start_permanent: Mix.env() == :prod,
1010
compilers: [:elixir_make] ++ Mix.compilers(),
1111
deps: deps(),

test/pg_query_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,30 @@ defmodule PgQueryTest do
3939
assert {:ok, query2} = PgQuery.protobuf_to_query(ast)
4040
assert query == query2
4141
end
42+
43+
test "parses and deparses a query with bang function" do
44+
query = "CREATE TABLE a (id int8 PRIMARY KEY)"
45+
ast = PgQuery.parse!(query)
46+
query2 = PgQuery.protobuf_to_query!(ast)
47+
assert query == query2
48+
end
49+
50+
test "scans a query" do
51+
query = "SELECT * FROM users WHERE id = 1"
52+
assert {:ok, scan_result} = PgQuery.scan(query)
53+
54+
# Basic structure validation
55+
assert %PgQuery.ScanResult{tokens: tokens} = scan_result
56+
assert is_list(tokens)
57+
assert length(tokens) == 8
58+
59+
# Check some expected tokens
60+
assert Enum.any?(tokens, fn token ->
61+
token.token == :SELECT
62+
end)
63+
64+
assert Enum.any?(tokens, fn token ->
65+
token.token == :FROM
66+
end)
67+
end
4268
end

0 commit comments

Comments
 (0)