Skip to content

Commit 108ce01

Browse files
authored
Merge pull request #70 from Recidiviz/dan/fix-positional-param
fix: allow positional parameters when typed query parameters are passed
2 parents d74d4bf + 0bd7df5 commit 108ce01

File tree

5 files changed

+306
-3
lines changed

5 files changed

+306
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ require (
118118
modernc.org/sqlite v1.37.0
119119
)
120120

121-
replace github.com/goccy/go-zetasqlite => github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20
121+
replace github.com/goccy/go-zetasqlite => github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
4242
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
4343
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
4444
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
45-
github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20 h1:xqkjZ/Q8KEjTp+WAiFA+r2bvbmdTkiSsMEE5mpQKWHk=
46-
github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20/go.mod h1:xtUAGxrJMK0vqv5Yj/AYvrcP3g338Tbh9oTyYk1VML8=
45+
github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20.1 h1:uf0WYvjEjgw+BqQTFMeC037To4hDXcslsAyqR8W397Y=
46+
github.com/Recidiviz/go-zetasqlite v0.18.0-recidiviz.20.1/go.mod h1:xtUAGxrJMK0vqv5Yj/AYvrcP3g338Tbh9oTyYk1VML8=
4747
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
4848
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
4949
github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4=

server/server_test.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4143,3 +4143,207 @@ func TestViewSchemaHydration(t *testing.T) {
41434143
}
41444144
}
41454145
}
4146+
4147+
// TestQueryWithPositionalParameters tests issue #69: https://github.com/Recidiviz/bigquery-emulator/issues/69
4148+
// Verifies that positional query parameters (?) work correctly and are not broken by allow_undeclared_parameters mode.
4149+
// The issue reports that v0.6.6-recidiviz.3.5 broke positional parameters because allow_undeclared_parameters was
4150+
// enabled globally. According to ZetaSQL docs: "When allow_undeclared_parameters is true, no positional parameters may be provided."
4151+
func TestQueryWithPositionalParameters(t *testing.T) {
4152+
const (
4153+
projectID = "test"
4154+
datasetID = "test_dataset"
4155+
tableID = "test_table"
4156+
)
4157+
4158+
ctx := context.Background()
4159+
4160+
bqServer, err := server.New(server.TempStorage)
4161+
if err != nil {
4162+
t.Fatal(err)
4163+
}
4164+
4165+
// Create test table with sample data for testing positional parameters
4166+
project := types.NewProject(
4167+
projectID,
4168+
types.NewDataset(
4169+
datasetID,
4170+
types.NewTable(
4171+
tableID,
4172+
[]*types.Column{
4173+
types.NewColumn("id", types.INTEGER),
4174+
types.NewColumn("name", types.STRING),
4175+
types.NewColumn("value", types.FLOAT),
4176+
types.NewColumn("active", types.BOOLEAN),
4177+
},
4178+
types.Data{
4179+
{"id": 1, "name": "Alice", "value": 10.5, "active": true},
4180+
{"id": 2, "name": "Bob", "value": 20.7, "active": false},
4181+
{"id": 3, "name": "Charlie", "value": 30.2, "active": true},
4182+
{"id": 4, "name": "David", "value": 40.9, "active": false},
4183+
{"id": 5, "name": "Eve", "value": 50.1, "active": true},
4184+
},
4185+
),
4186+
),
4187+
)
4188+
if err := bqServer.Load(server.StructSource(project)); err != nil {
4189+
t.Fatal(err)
4190+
}
4191+
4192+
testServer := bqServer.TestServer()
4193+
defer func() {
4194+
testServer.Close()
4195+
bqServer.Close()
4196+
}()
4197+
4198+
client, err := bigquery.NewClient(
4199+
ctx,
4200+
projectID,
4201+
option.WithEndpoint(testServer.URL),
4202+
option.WithoutAuthentication(),
4203+
)
4204+
if err != nil {
4205+
t.Fatal(err)
4206+
}
4207+
defer client.Close()
4208+
4209+
t.Run("Multiple positional parameters", func(t *testing.T) {
4210+
query := client.Query(fmt.Sprintf("SELECT id, name, value FROM `%s.%s.%s` WHERE id >= ? AND id <= ? ORDER BY id", projectID, datasetID, tableID))
4211+
query.Parameters = []bigquery.QueryParameter{
4212+
{Value: 2},
4213+
{Value: 4},
4214+
}
4215+
4216+
it, err := query.Read(ctx)
4217+
if err != nil {
4218+
t.Fatalf("Query failed: %v", err)
4219+
}
4220+
4221+
var rows [][]bigquery.Value
4222+
for {
4223+
var row []bigquery.Value
4224+
if err := it.Next(&row); err != nil {
4225+
if err == iterator.Done {
4226+
break
4227+
}
4228+
t.Fatal(err)
4229+
}
4230+
rows = append(rows, row)
4231+
}
4232+
4233+
if len(rows) != 3 {
4234+
t.Fatalf("Expected 3 rows, got %d", len(rows))
4235+
}
4236+
// Verify we got ids 2, 3, 4
4237+
expectedIDs := []int64{2, 3, 4}
4238+
for i, row := range rows {
4239+
if row[0].(int64) != expectedIDs[i] {
4240+
t.Errorf("Row %d: expected id=%d, got %v", i, expectedIDs[i], row[0])
4241+
}
4242+
}
4243+
})
4244+
4245+
t.Run("Positional parameters with different types", func(t *testing.T) {
4246+
query := client.Query(fmt.Sprintf("SELECT id, name FROM `%s.%s.%s` WHERE value > ? AND active = ? ORDER BY id", projectID, datasetID, tableID))
4247+
query.Parameters = []bigquery.QueryParameter{
4248+
{Value: 25.0}, // Float
4249+
{Value: true}, // Boolean
4250+
}
4251+
4252+
it, err := query.Read(ctx)
4253+
if err != nil {
4254+
t.Fatalf("Query failed: %v", err)
4255+
}
4256+
4257+
var rows [][]bigquery.Value
4258+
for {
4259+
var row []bigquery.Value
4260+
if err := it.Next(&row); err != nil {
4261+
if err == iterator.Done {
4262+
break
4263+
}
4264+
t.Fatal(err)
4265+
}
4266+
rows = append(rows, row)
4267+
}
4268+
4269+
// Should get Charlie (value=30.2, active=true) and Eve (value=50.1, active=true)
4270+
if len(rows) != 2 {
4271+
t.Fatalf("Expected 2 rows, got %d", len(rows))
4272+
}
4273+
expectedNames := []string{"Charlie", "Eve"}
4274+
for i, row := range rows {
4275+
if row[1].(string) != expectedNames[i] {
4276+
t.Errorf("Row %d: expected name='%s', got %v", i, expectedNames[i], row[1])
4277+
}
4278+
}
4279+
})
4280+
4281+
t.Run("Positional parameter in LIMIT clause", func(t *testing.T) {
4282+
query := client.Query(fmt.Sprintf("SELECT id, name FROM `%s.%s.%s` ORDER BY id LIMIT ?", projectID, datasetID, tableID))
4283+
query.Parameters = []bigquery.QueryParameter{
4284+
{Value: 2},
4285+
}
4286+
4287+
it, err := query.Read(ctx)
4288+
if err != nil {
4289+
t.Fatalf("Query failed: %v", err)
4290+
}
4291+
4292+
var rows [][]bigquery.Value
4293+
for {
4294+
var row []bigquery.Value
4295+
if err := it.Next(&row); err != nil {
4296+
if err == iterator.Done {
4297+
break
4298+
}
4299+
t.Fatal(err)
4300+
}
4301+
rows = append(rows, row)
4302+
}
4303+
4304+
if len(rows) != 2 {
4305+
t.Fatalf("Expected 2 rows, got %d", len(rows))
4306+
}
4307+
// Should get first 2 rows: Alice and Bob
4308+
if rows[0][1].(string) != "Alice" {
4309+
t.Errorf("Expected first row name='Alice', got %v", rows[0][1])
4310+
}
4311+
if rows[1][1].(string) != "Bob" {
4312+
t.Errorf("Expected second row name='Bob', got %v", rows[1][1])
4313+
}
4314+
})
4315+
4316+
t.Run("Positional parameter with string type", func(t *testing.T) {
4317+
query := client.Query(fmt.Sprintf("SELECT id, name FROM `%s.%s.%s` WHERE name = ? ORDER BY id", projectID, datasetID, tableID))
4318+
query.Parameters = []bigquery.QueryParameter{
4319+
{Value: "Bob"},
4320+
}
4321+
4322+
it, err := query.Read(ctx)
4323+
if err != nil {
4324+
t.Fatalf("Query failed: %v", err)
4325+
}
4326+
4327+
var rows [][]bigquery.Value
4328+
for {
4329+
var row []bigquery.Value
4330+
if err := it.Next(&row); err != nil {
4331+
if err == iterator.Done {
4332+
break
4333+
}
4334+
t.Fatal(err)
4335+
}
4336+
rows = append(rows, row)
4337+
}
4338+
4339+
if len(rows) != 1 {
4340+
t.Fatalf("Expected 1 row, got %d", len(rows))
4341+
}
4342+
if rows[0][0].(int64) != 2 {
4343+
t.Errorf("Expected id=2, got %v", rows[0][0])
4344+
}
4345+
if rows[0][1].(string) != "Bob" {
4346+
t.Errorf("Expected name='Bob', got %v", rows[0][1])
4347+
}
4348+
})
4349+
}

test/node/src/parameterizedQueries.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,54 @@ describe('Parameterized Queries (Issue #58)', () => {
518518
expect(rows).toHaveLength(5);
519519
});
520520
});
521+
522+
describe('Issue #69: Positional Parameters', () => {
523+
beforeAll(async () => {
524+
// Create a test table for positional parameter testing
525+
await helper.createTable(
526+
{ datasetId: 'test_dataset', tableId: 'positional_test' },
527+
{
528+
fields: [
529+
{ name: 'id', type: 'INTEGER', mode: 'REQUIRED' },
530+
{ name: 'name', type: 'STRING', mode: 'REQUIRED' },
531+
],
532+
}
533+
);
534+
535+
// Insert test data
536+
await helper.insertRows(
537+
{ datasetId: 'test_dataset', tableId: 'positional_test' },
538+
[
539+
{ id: 1, name: 'Alice' },
540+
{ id: 2, name: 'Bob' },
541+
{ id: 3, name: 'Charlie' },
542+
]
543+
);
544+
});
545+
546+
it('should handle positional parameter (?) in WHERE clause', async () => {
547+
// Tests resolution of https://github.com/Recidiviz/bigquery-emulator/issues/69
548+
// Verifies that positional query parameters work correctly and are not broken by allow_undeclared_parameters.
549+
// According to ZetaSQL docs: "When allow_undeclared_parameters is true, no positional parameters may be provided."
550+
551+
const query = `
552+
SELECT id, name
553+
FROM \`${BQ_EMULATOR_PROJECT_ID}.test_dataset.positional_test\`
554+
WHERE id = ?
555+
ORDER BY id
556+
`;
557+
558+
// Positional parameters are passed as an array without names
559+
const options = {
560+
query,
561+
params: [2], // Positional parameter value
562+
};
563+
564+
const [rows] = await client.query(options);
565+
566+
expect(rows).toHaveLength(1);
567+
expect(rows[0].id).toBe(2);
568+
expect(rows[0].name).toBe('Bob');
569+
});
570+
});
521571
});

test/python/emulator_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,3 +1797,52 @@ def test_null_numeric_parameter(self) -> None:
17971797
],
17981798
job_config=job_config,
17991799
)
1800+
1801+
def test_positional_query_parameters(self) -> None:
1802+
"""Tests resolution of https://github.com/Recidiviz/bigquery-emulator/issues/69
1803+
1804+
Tests that positional query parameters (?) work correctly and are not broken
1805+
by allow_undeclared_parameters mode. The issue reported that v0.6.6-recidiviz.3.5
1806+
broke positional parameters because allow_undeclared_parameters was enabled globally.
1807+
According to ZetaSQL docs: "When allow_undeclared_parameters is true, no positional
1808+
parameters may be provided."
1809+
"""
1810+
address = BigQueryAddress(dataset_id=_DATASET_1, table_id="positional_params_test")
1811+
self.create_mock_table(
1812+
address,
1813+
schema=[
1814+
bigquery.SchemaField("id", bigquery.enums.SqlTypeNames.INTEGER.value, mode="REQUIRED"),
1815+
bigquery.SchemaField("name", bigquery.enums.SqlTypeNames.STRING.value, mode="REQUIRED"),
1816+
],
1817+
)
1818+
self.load_rows_into_table(
1819+
address,
1820+
data=[
1821+
{"id": 1, "name": "Alice"},
1822+
{"id": 2, "name": "Bob"},
1823+
{"id": 3, "name": "Charlie"},
1824+
],
1825+
)
1826+
1827+
# Test single positional parameter in WHERE clause
1828+
query = f"""
1829+
SELECT id, name
1830+
FROM `{self.project_id}.{address.dataset_id}.{address.table_id}`
1831+
WHERE id = ?
1832+
ORDER BY id
1833+
"""
1834+
1835+
# Positional parameters are specified using ScalarQueryParameter with name=None
1836+
job_config = bigquery.QueryJobConfig(
1837+
query_parameters=[
1838+
bigquery.ScalarQueryParameter(None, "INT64", 2)
1839+
]
1840+
)
1841+
1842+
self.run_query_test(
1843+
query,
1844+
expected_result=[
1845+
{"id": 2, "name": "Bob"},
1846+
],
1847+
job_config=job_config,
1848+
)

0 commit comments

Comments
 (0)