Skip to content

Commit 748e0ae

Browse files
Extract BigQueryClientProvider for injectable BQ clients
1 parent 26a2a81 commit 748e0ae

2 files changed

Lines changed: 117 additions & 31 deletions

File tree

cli/api/dbadapters/bigquery.ts

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,55 @@ export interface IBigQueryExecutionOptions {
3737
reservation?: string;
3838
}
3939

40+
export interface IBigQueryClientProvider {
41+
get(projectId?: string): BigQuery;
42+
}
43+
44+
export class BigQueryClientProvider implements IBigQueryClientProvider {
45+
private readonly clients = new Map<string, BigQuery>();
46+
47+
constructor(private readonly credentials: dataform.IBigQuery) {}
48+
49+
public get(projectId?: string): BigQuery {
50+
projectId = projectId || this.credentials.projectId;
51+
if (!this.clients.has(projectId)) {
52+
this.clients.set(
53+
projectId,
54+
new BigQuery({
55+
projectId,
56+
scopes: EXTRA_GOOGLE_SCOPES,
57+
location: this.credentials.location,
58+
credentials: this.credentials.credentials && JSON.parse(this.credentials.credentials)
59+
})
60+
);
61+
}
62+
return this.clients.get(projectId);
63+
}
64+
}
65+
66+
export class StaticBigQueryClientProvider implements IBigQueryClientProvider {
67+
constructor(private readonly client: BigQuery) {}
68+
69+
public get(projectId?: string): BigQuery {
70+
return this.client;
71+
}
72+
}
73+
4074
export class BigQueryDbAdapter implements IDbAdapter {
4175
private bigQueryCredentials: dataform.IBigQuery;
4276
private pool: PromisePoolExecutor;
43-
44-
private readonly clients = new Map<string, BigQuery>();
45-
private readonly bigqueryClient?: BigQuery;
77+
private clientProvider: IBigQueryClientProvider;
4678

4779
constructor(
4880
credentials: dataform.IBigQuery,
49-
options?: { concurrencyLimit?: number; bigqueryClient?: BigQuery }
81+
options?: {
82+
concurrencyLimit?: number;
83+
clientProvider?: IBigQueryClientProvider;
84+
}
5085
) {
5186
this.bigQueryCredentials = credentials;
52-
this.bigqueryClient = options?.bigqueryClient;
87+
this.clientProvider = options?.clientProvider || new BigQueryClientProvider(credentials);
88+
5389
// Bigquery allows 50 concurrent queries, and a rate limit of 100/user/second by default.
5490
// These limits should be safely low enough for most projects.
5591
this.pool = new PromisePoolExecutor({
@@ -174,7 +210,7 @@ export class BigQueryDbAdapter implements IDbAdapter {
174210
datasetIds.map(async datasetId => {
175211
const [tables] = await this.getClient(database)
176212
.dataset(datasetId)
177-
.getTables();
213+
.getTables({ autoPaginate: true, maxResults: 1000 });
178214
await Promise.all(
179215
tables.map(async table => {
180216
const metadata = await this.table({
@@ -272,14 +308,14 @@ export class BigQueryDbAdapter implements IDbAdapter {
272308
}
273309

274310
public async schemas(database: string): Promise<string[]> {
275-
const data = await this.getClient(database).getDatasets();
311+
const data = await this.getClient(database).getDatasets({ autoPaginate: true, maxResults: 1000 });
276312
return data[0].map(dataset => dataset.id);
277313
}
278314

279315
public async createSchema(database: string, schema: string): Promise<void> {
280316
await this.execute(
281317
`create schema if not exists \`${database || this.bigQueryCredentials.projectId}.${schema}\``,
282-
{ bigquery: { location: this.bigQueryCredentials.location } }
318+
{ bigquery: { location: this.bigQueryCredentials.location || "US" } }
283319
);
284320
}
285321

@@ -320,23 +356,7 @@ export class BigQueryDbAdapter implements IDbAdapter {
320356
}
321357

322358
private getClient(projectId?: string) {
323-
if (this.bigqueryClient) {
324-
return this.bigqueryClient;
325-
}
326-
projectId = projectId || this.bigQueryCredentials.projectId;
327-
if (!this.clients.has(projectId)) {
328-
this.clients.set(
329-
projectId,
330-
new BigQuery({
331-
projectId,
332-
scopes: EXTRA_GOOGLE_SCOPES,
333-
location: this.bigQueryCredentials.location,
334-
credentials:
335-
this.bigQueryCredentials.credentials && JSON.parse(this.bigQueryCredentials.credentials)
336-
})
337-
);
338-
}
339-
return this.clients.get(projectId);
359+
return this.clientProvider.get(projectId);
340360
}
341361

342362
private async runQuery(
@@ -544,11 +564,14 @@ function convertFieldType(type: string) {
544564
case "INT64":
545565
return dataform.Field.Primitive.INTEGER;
546566
case "NUMERIC":
567+
case "BIGNUMERIC":
547568
return dataform.Field.Primitive.NUMERIC;
548569
case "BOOL":
549570
case "BOOLEAN":
550571
return dataform.Field.Primitive.BOOLEAN;
551572
case "STRING":
573+
case "JSON":
574+
case "INTERVAL":
552575
return dataform.Field.Primitive.STRING;
553576
case "DATE":
554577
return dataform.Field.Primitive.DATE;

cli/api/dbadapters/bigquery_test.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Dataset, Table } from "@google-cloud/bigquery";
22
import { expect } from "chai";
33
import { anything, instance, mock, verify, when } from "ts-mockito";
44

5-
import { BigQueryDbAdapter } from "df/cli/api/dbadapters/bigquery";
5+
import { BigQueryDbAdapter, StaticBigQueryClientProvider } from "df/cli/api/dbadapters/bigquery";
66
import { dataform } from "df/protos/ts";
77
import { suite, test } from "df/testing";
88

@@ -17,12 +17,12 @@ suite("BigQueryDbAdapter", () => {
1717
const projectId = "project1";
1818

1919
const credentials = dataform.BigQuery.create({ projectId, location: "US" });
20-
const adapter = new BigQueryDbAdapter(credentials, { bigqueryClient: instance(mockBigQuery) });
20+
const adapter = new BigQueryDbAdapter(credentials, { clientProvider: new StaticBigQueryClientProvider(instance(mockBigQuery)) });
2121

2222
when(mockBigQuery.dataset(schemaName)).thenReturn(instance(mockDataset));
2323
// getTables returns an array where the first element is an array of tables.
2424
// Each table object needs an 'id' property.
25-
when(mockDataset.getTables()).thenReturn(Promise.resolve([[{ id: tableName }]] as any));
25+
when(mockDataset.getTables(anything())).thenReturn(Promise.resolve([[{ id: tableName }]] as any));
2626
when(mockDataset.table(tableName)).thenReturn(instance(mockTable));
2727
when(mockTable.getMetadata()).thenReturn(
2828
Promise.resolve([
@@ -54,10 +54,10 @@ suite("BigQueryDbAdapter", () => {
5454
const projectId = "project";
5555

5656
const credentials = dataform.BigQuery.create({ projectId, location: "US" });
57-
const adapter = new BigQueryDbAdapter(credentials, { bigqueryClient: instance(mockBigQuery) });
57+
const adapter = new BigQueryDbAdapter(credentials, { clientProvider: new StaticBigQueryClientProvider(instance(mockBigQuery)) });
5858

5959
when(mockBigQuery.dataset(schemaName)).thenReturn(instance(mockDataset));
60-
when(mockDataset.getTables()).thenReturn(Promise.resolve([[{ id: tableName }]] as any));
60+
when(mockDataset.getTables(anything())).thenReturn(Promise.resolve([[{ id: tableName }]] as any));
6161
when(mockDataset.table(tableName)).thenReturn(instance(mockTable));
6262
when(mockTable.getMetadata()).thenReturn(
6363
Promise.resolve([
@@ -70,7 +70,7 @@ suite("BigQueryDbAdapter", () => {
7070
] as any)
7171
);
7272

73-
when(mockBigQuery.getDatasets()).thenReturn(Promise.resolve([[{ id: schemaName }]] as any));
73+
when(mockBigQuery.getDatasets(anything())).thenReturn(Promise.resolve([[{ id: schemaName }]] as any));
7474

7575
const result = await adapter.tables(projectId);
7676

@@ -79,4 +79,67 @@ suite("BigQueryDbAdapter", () => {
7979
expect(result[0].target.schema).to.equal(schemaName);
8080
expect(result[0].target.name).to.equal(tableName);
8181
});
82+
83+
test("setMetadata handles action without columns", async () => {
84+
// Partial mock for BigQuery client to avoid real network calls
85+
const mockBigQuery: any = {
86+
dataset: () => ({
87+
table: () => ({
88+
getMetadata: () => Promise.resolve([{ schema: { fields: [] } }]),
89+
setMetadata: (metadata: any) => {
90+
expect(metadata.description).to.equal("test");
91+
return Promise.resolve([]);
92+
}
93+
})
94+
})
95+
};
96+
97+
const credentials = dataform.BigQuery.create({ projectId: "p", location: "US" });
98+
const adapter = new BigQueryDbAdapter(credentials, {
99+
concurrencyLimit: 1,
100+
clientProvider: { get: () => mockBigQuery }
101+
});
102+
103+
const action = dataform.ExecutionAction.create({
104+
target: { database: "db", schema: "sch", name: "tab" },
105+
actionDescriptor: { description: "test" }
106+
// columns is missing/null in this action
107+
});
108+
109+
// This should not throw "cannot read property 'find' of undefined"
110+
await adapter.setMetadata(action);
111+
});
112+
113+
test("setMetadata correctly maps column descriptions", async () => {
114+
const mockBigQuery: any = {
115+
dataset: () => ({
116+
table: () => ({
117+
getMetadata: () => Promise.resolve([{
118+
schema: {
119+
fields: [{ name: "id", type: "INTEGER" }]
120+
}
121+
}]),
122+
setMetadata: (metadata: any) => {
123+
expect(metadata.schema[0].description).to.equal("id desc");
124+
return Promise.resolve([]);
125+
}
126+
})
127+
})
128+
};
129+
130+
const credentials = dataform.BigQuery.create({ projectId: "p", location: "US" });
131+
const adapter = new BigQueryDbAdapter(credentials, {
132+
concurrencyLimit: 1,
133+
clientProvider: { get: () => mockBigQuery }
134+
});
135+
136+
const action = dataform.ExecutionAction.create({
137+
target: { database: "db", schema: "sch", name: "tab" },
138+
actionDescriptor: {
139+
columns: [{ path: ["id"], description: "id desc" }]
140+
}
141+
});
142+
143+
await adapter.setMetadata(action);
144+
});
82145
});

0 commit comments

Comments
 (0)