Line data Source code
1 : #include "backend/engine/duckdb/transpiler/transpiler_test_fixture.h"
2 :
3 : namespace bigquery_emulator {
4 : namespace backend {
5 : namespace engine {
6 : namespace duckdb {
7 : namespace transpiler {
8 :
9 1 : TEST_F(TranspilerTest, EmitLiteralInt64) {
10 1 : const ::googlesql::ResolvedStatement* stmt = Analyze("SELECT 42 AS n");
11 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
12 1 : ASSERT_NE(expr, nullptr);
13 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_LITERAL);
14 1 : TestTranspiler t;
15 1 : EXPECT_EQ(t.EmitLiteral(expr->GetAs<::googlesql::ResolvedLiteral>()), "42");
16 1 : }
17 :
18 1 : TEST_F(TranspilerTest, EmitLiteralString) {
19 1 : const ::googlesql::ResolvedStatement* stmt = Analyze("SELECT 'hi' AS s");
20 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
21 1 : ASSERT_NE(expr, nullptr);
22 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_LITERAL);
23 1 : TestTranspiler t;
24 : // We must emit single-quoted strings: DuckDB reads double-quoted
25 : // text as an *identifier*, so a `"hi"` literal would be a column
26 : // reference rather than a string. EmitLiteral overrides
27 : // GoogleSQL's default double-quoted form on TYPE_STRING for
28 : // exactly this reason -- otherwise every string-bearing query
29 : // would surface UNIMPLEMENTED instead of a real result.
30 1 : EXPECT_EQ(t.EmitLiteral(expr->GetAs<::googlesql::ResolvedLiteral>()), "'hi'");
31 1 : }
32 :
33 1 : TEST_F(TranspilerTest, EmitLiteralBoolTrue) {
34 1 : const ::googlesql::ResolvedStatement* stmt = Analyze("SELECT TRUE AS b");
35 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
36 1 : ASSERT_NE(expr, nullptr);
37 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_LITERAL);
38 1 : TestTranspiler t;
39 1 : EXPECT_EQ(t.EmitLiteral(expr->GetAs<::googlesql::ResolvedLiteral>()), "true");
40 1 : }
41 :
42 1 : TEST_F(TranspilerTest, EmitColumnRefQuotesIdentifier) {
43 : // The analyzer collapses a bare `SELECT id FROM people` straight
44 : // onto the TableScan (no wrapping ProjectScan), so to land a
45 : // standalone `ResolvedColumnRef` we wrap the column in a
46 : // function call. `COALESCE(id, 0)` keeps the test focused: the
47 : // first argument is the ColumnRef we want to assert on.
48 1 : const ::googlesql::ResolvedStatement* stmt =
49 1 : Analyze("SELECT COALESCE(id, 0) FROM people");
50 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
51 1 : ASSERT_NE(expr, nullptr);
52 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
53 1 : const ::googlesql::ResolvedFunctionCall* call =
54 1 : expr->GetAs<::googlesql::ResolvedFunctionCall>();
55 1 : ASSERT_GE(call->argument_list_size(), 1);
56 1 : const ::googlesql::ResolvedExpr* arg = call->argument_list(0);
57 1 : ASSERT_NE(arg, nullptr);
58 1 : ASSERT_EQ(arg->node_kind(), ::googlesql::RESOLVED_COLUMN_REF);
59 1 : TestTranspiler t;
60 1 : EXPECT_EQ(t.EmitColumnRef(arg->GetAs<::googlesql::ResolvedColumnRef>()),
61 1 : "\"id\"");
62 1 : }
63 :
64 1 : TEST_F(TranspilerTest, EmitFunctionCallCoalesce) {
65 1 : const ::googlesql::ResolvedStatement* stmt =
66 1 : Analyze("SELECT COALESCE(name, 'unknown') AS n FROM people");
67 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
68 1 : ASSERT_NE(expr, nullptr);
69 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
70 1 : TestTranspiler t;
71 1 : EXPECT_EQ(
72 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()),
73 1 : "COALESCE(\"name\", 'unknown')");
74 1 : }
75 :
76 1 : TEST_F(TranspilerTest, EmitFunctionCallIfnull) {
77 1 : const ::googlesql::ResolvedStatement* stmt =
78 1 : Analyze("SELECT IFNULL(name, 'unknown') AS n FROM people");
79 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
80 1 : ASSERT_NE(expr, nullptr);
81 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
82 1 : TestTranspiler t;
83 1 : EXPECT_EQ(
84 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()),
85 1 : "IFNULL(\"name\", 'unknown')");
86 1 : }
87 :
88 1 : TEST_F(TranspilerTest, EmitFunctionCallNonLoweringDispositionReturnsEmpty) {
89 : // `BIT_COUNT` is on the `semantic_executor` route in
90 : // `functions.yaml` (BQ flavor differs from DuckDB's `bit_count`)
91 : // with the body deferred to `docs/ENGINE_POLICY.md`.
92 : // The transpiler has no DuckDB lowering for this disposition, so
93 : // the emit returns "" and the engine surfaces UNIMPLEMENTED for
94 : // the whole query.
95 1 : const ::googlesql::ResolvedStatement* stmt =
96 1 : Analyze("SELECT BIT_COUNT(id) AS n FROM people");
97 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
98 1 : ASSERT_NE(expr, nullptr);
99 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
100 1 : TestTranspiler t;
101 1 : EXPECT_EQ(
102 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()), "");
103 1 : }
104 :
105 1 : TEST_F(TranspilerTest, EmitFunctionCallMappedFunction) {
106 : // Disposition-table-backed scalars: `ABS(id)` lowers to DuckDB's
107 : // `ABS(...)`. The casing of the emitted function name comes from
108 : // the YAML disposition (we render the duckdb_name verbatim); the
109 : // BQ-side `abs` lookup is case-insensitive.
110 1 : const ::googlesql::ResolvedStatement* stmt =
111 1 : Analyze("SELECT ABS(id) AS n FROM people");
112 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
113 1 : ASSERT_NE(expr, nullptr);
114 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
115 1 : TestTranspiler t;
116 1 : EXPECT_EQ(
117 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()),
118 1 : "ABS(\"id\")");
119 1 : }
120 :
121 1 : TEST_F(TranspilerTest, EmitFunctionCallLengthMaps) {
122 : // `LENGTH(name)` -> `LENGTH("name")`. Two-arg variants don't exist
123 : // for LENGTH in either dialect; the single-arg shape is the entire
124 : // disposition surface.
125 1 : const ::googlesql::ResolvedStatement* stmt =
126 1 : Analyze("SELECT LENGTH(name) AS n FROM people");
127 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
128 1 : ASSERT_NE(expr, nullptr);
129 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
130 1 : TestTranspiler t;
131 1 : EXPECT_EQ(
132 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()),
133 1 : "LENGTH(\"name\")");
134 1 : }
135 :
136 1 : TEST_F(TranspilerTest, EmitFunctionCallReadyDuckdbUdf) {
137 : // Ready `duckdb_udf` rows emit identically to `duckdb_native`:
138 : // the transpiler renders `<duckdb_name>(<args>)` and DuckDB
139 : // resolves the call to the registered polyfill macro. `MOD`
140 : // flipped to ready in the numeric-family commit; the row carries
141 : // `duckdb_name=bq_mod`.
142 1 : const ::googlesql::ResolvedStatement* stmt =
143 1 : Analyze("SELECT MOD(id, 3) AS m FROM people");
144 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
145 1 : ASSERT_NE(expr, nullptr);
146 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
147 1 : TestTranspiler t;
148 1 : EXPECT_EQ(
149 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()),
150 1 : "bq_mod(\"id\", 3)");
151 1 : }
152 :
153 1 : TEST_F(TranspilerTest, EmitFunctionCallSafeModeReturnsEmpty) {
154 : // SAFE.<fn>(...) sets `error_mode = SAFE_ERROR_MODE`. DuckDB has no
155 : // native SAFE analog yet, so the emit short-circuits to "" before
156 : // consulting the disposition table -- this would otherwise emit
157 : // ABS("id") and silently lose the SAFE error semantics.
158 1 : const ::googlesql::ResolvedStatement* stmt =
159 1 : Analyze("SELECT SAFE.ABS(id) AS n FROM people");
160 1 : const ::googlesql::ResolvedExpr* expr = QueryFirstSelectExpr(stmt);
161 1 : ASSERT_NE(expr, nullptr);
162 1 : ASSERT_EQ(expr->node_kind(), ::googlesql::RESOLVED_FUNCTION_CALL);
163 1 : TestTranspiler t;
164 1 : EXPECT_EQ(
165 1 : t.EmitFunctionCall(expr->GetAs<::googlesql::ResolvedFunctionCall>()), "");
166 1 : }
167 :
168 : } // namespace transpiler
169 : } // namespace duckdb
170 : } // namespace engine
171 : } // namespace backend
172 : } // namespace bigquery_emulator
|