diff --git a/docker/bigquery/data.yaml b/docker/bigquery/data.yaml index 13b93aa469..84157bb3a6 100644 --- a/docker/bigquery/data.yaml +++ b/docker/bigquery/data.yaml @@ -85,6 +85,8 @@ projects: type: DATE - id: flags columns: + - name: id + type: INT64 - name: attachment_id type: INT64 - name: bug_id @@ -105,6 +107,8 @@ projects: type: DATE - id: tracking_flags columns: + - name: id + type: INT64 - name: bug_id type: INT64 - name: name diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index 2800825075..52f97a48d0 100755 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -1396,12 +1396,6 @@ sub db_schema_abstract_schema { bmo_etl_cache_uniq_idx => {FIELDS => ['id', 'table_name'], TYPE => 'UNIQUE'} ], }; - $args->{schema}->{bmo_etl_locked} = { - FIELDS => [ - value => {TYPE => 'VARCHAR(20)', NOTNULL => 1,}, - creation_ts => {TYPE => 'DATETIME',}, - ], - }; } sub install_update_db { @@ -1589,12 +1583,6 @@ sub install_update_db { }); } - # Add bmo_etl_locked.creation_ts column - if (!$dbh->bz_column_info('bmo_etl_locked', 'creation_ts')) { - $dbh->bz_add_column('bmo_etl_locked', - 'creation_ts' => {TYPE => 'DATETIME'}); - } - # Add unique index for id and table name for bmo_etl_cache $dbh->bz_add_index('bmo_etl_cache', 'bmo_etl_cache_uniq_idx', {FIELDS => ['id', 'table_name'], TYPE => 'UNIQUE'}); diff --git a/extensions/BMO/bin/export_bmo_etl.pl b/extensions/BMO/bin/export_bmo_etl.pl index e25029d6aa..905d3a1eda 100644 --- a/extensions/BMO/bin/export_bmo_etl.pl +++ b/extensions/BMO/bin/export_bmo_etl.pl @@ -19,6 +19,7 @@ use Bugzilla::Group; use Bugzilla::Logging; use Bugzilla::User; +use Bugzilla::Util qw(with_writable_database); use Bugzilla::Extension::Review::FlagStateActivity; use HTTP::Headers; @@ -65,11 +66,9 @@ my $dataset_id = Bugzilla->params->{bmo_etl_dataset_id}; $dataset_id || die "Invalid BigQuery dataset ID.\n"; -# Check to make sure another instance is not currently running -check_and_set_lock(); - # Use replica if available my $dbh = Bugzilla->switch_to_shadow_db(); +$dbh->bz_start_transaction(); my $ua = LWP::UserAgent::Determined->new( agent => 'Bugzilla', @@ -94,10 +93,6 @@ # Bugs that are private to one or more groups our %private_bugs = (); -# In order to avoid entering duplicate data, we will first query BigQuery -# to make sure other entries with this date are not already present. -check_for_duplicates(); - # Process each table to be sent to ETL process_bugs(); process_attachments(); @@ -129,8 +124,7 @@ ['bug_id', 'duplicate_of_id'] ); -# If we are done, remove the lock -delete_lock(); +$dbh->bz_commit_transaction(); ### Functions @@ -139,18 +133,22 @@ sub process_bugs { my $count = 0; my $last_offset = 0; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs'); + # Retrieve the max ID from BQ in case we didn'complete last time + my $max_id = get_max_id($table_name); + + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs WHERE bug_id > ?', + undef, $max_id); logger("Processing $total $table_name"); my $sth = $dbh->prepare( - 'SELECT bug_id AS id, delta_ts AS modification_time FROM bugs ORDER BY bug_id LIMIT ? OFFSET ?' + 'SELECT bug_id AS id, delta_ts AS modification_time FROM bugs WHERE bug_id > ? ORDER BY bug_id LIMIT ? OFFSET ?' ); while ($count < $total) { my @bugs = (); - $sth->execute(API_BLOCK_COUNT, $last_offset); + $sth->execute($max_id, API_BLOCK_COUNT, $last_offset); while (my ($id, $mod_time) = $sth->fetchrow_array()) { logger("Processing id $id with mod_time of $mod_time."); @@ -159,7 +157,8 @@ sub process_bugs { my $data = get_cache($id, $table_name, $mod_time); if (!$data) { - logger("$table_name id $id with time $mod_time not found in cache.", DEBUG_OUTPUT); + logger("$table_name id $id with time $mod_time not found in cache.", + DEBUG_OUTPUT); my $obj = Bugzilla::Bug->new($id); @@ -249,18 +248,24 @@ sub process_attachments { my $count = 0; my $last_offset = 0; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM attachments'); + # Retrieve the max ID from BQ in case we didn'complete last time + my $max_id = get_max_id($table_name); + + my $total + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM attachments WHERE attach_id > ?', + undef, $max_id); logger("Processing $total $table_name."); my $sth = $dbh->prepare( - 'SELECT attach_id, modification_time FROM attachments ORDER BY attach_id LIMIT ? OFFSET ?' + 'SELECT attach_id, modification_time FROM attachments WHERE attach_id > ? ORDER BY attach_id LIMIT ? OFFSET ?' ); while ($count < $total) { my @results = (); - $sth->execute(API_BLOCK_COUNT, $last_offset); + $sth->execute($max_id, API_BLOCK_COUNT, $last_offset); while (my ($id, $mod_time) = $sth->fetchrow_array()) { logger("Processing id $id with mod_time of $mod_time."); @@ -269,7 +274,8 @@ sub process_attachments { my $data = get_cache($id, $table_name, $mod_time); if (!$data) { - logger("$table_name id $id with time $mod_time not found in cache." , DEBUG_OUTPUT); + logger("$table_name id $id with time $mod_time not found in cache.", + DEBUG_OUTPUT); my $obj = Bugzilla::Attachment->new($id); @@ -315,16 +321,22 @@ sub process_flags { my $count = 0; my $last_offset = 0; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM flags'); + # Retrieve the max ID from BQ in case we didn'complete last time + my $max_id = get_max_id($table_name); + + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM flags WHERE id > ?', + undef, $max_id); logger("Processing $total $table_name."); - my $sth = $dbh->prepare( - 'SELECT id, modification_date FROM flags ORDER BY id LIMIT ? OFFSET ?'); + my $sth + = $dbh->prepare( + 'SELECT id, modification_date FROM flags WHERE id > ? ORDER BY id LIMIT ? OFFSET ?' + ); while ($count < $total) { my @results = (); - $sth->execute(API_BLOCK_COUNT, $last_offset); + $sth->execute($max_id, API_BLOCK_COUNT, $last_offset); while (my ($id, $mod_time) = $sth->fetchrow_array()) { logger("Processing id $id with mod_time of $mod_time."); @@ -333,7 +345,8 @@ sub process_flags { my $data = get_cache($id, $table_name, $mod_time); if (!$data) { - logger("$table_name id $id with time $mod_time not found in cache." , DEBUG_OUTPUT); + logger("$table_name id $id with time $mod_time not found in cache.", + DEBUG_OUTPUT); my $obj = Bugzilla::Flag->new($id); @@ -343,6 +356,7 @@ sub process_flags { } $data = { + id => $obj->id, attachment_id => $obj->attach_id || undef, bug_id => $obj->bug_id, creation_ts => $obj->creation_date, @@ -402,7 +416,8 @@ sub process_flag_state_activity { my $data = get_cache($id, $table_name, $mod_time); if (!$data) { - logger("$table_name id $id with time $mod_time not found in cache.", DEBUG_OUTPUT); + logger("$table_name id $id with time $mod_time not found in cache.", + DEBUG_OUTPUT); my $obj = Bugzilla::Extension::Review::FlagStateActivity->new($id); @@ -443,36 +458,42 @@ sub process_tracking_flags { my $count = 0; my $last_offset = 0; + # Retrieve the max ID from BQ in case we didn'complete last time + my $max_id = get_max_id($table_name); + my $total = $dbh->selectrow_array( 'SELECT COUNT(*) FROM tracking_flags_bugs JOIN tracking_flags ON tracking_flags_bugs.tracking_flag_id = tracking_flags.id - ORDER BY tracking_flags_bugs.bug_id' + WHERE tracking_flags_bugs.id > ? + ORDER BY tracking_flags_bugs.bug_id', undef, $max_id ); + logger("Processing $total $table_name."); my $sth = $dbh->prepare( - 'SELECT tracking_flags.name, tracking_flags_bugs.bug_id, tracking_flags_bugs.value + 'SELECT tracking_flags_bugs.id, tracking_flags.name, tracking_flags_bugs.bug_id, tracking_flags_bugs.value FROM tracking_flags_bugs JOIN tracking_flags ON tracking_flags_bugs.tracking_flag_id = tracking_flags.id - ORDER BY tracking_flags_bugs.id LIMIT ? OFFSET ?' + WHERE tracking_flags_bugs.id > ? + ORDER BY tracking_flags_bugs.id LIMIT ? OFFSET ?' ); while ($count < $total) { my @results = (); - $sth->execute(API_BLOCK_COUNT, $last_offset); + $sth->execute($max_id, API_BLOCK_COUNT, $last_offset); - while (my ($name, $bug_id, $value) = $sth->fetchrow_array()) { + while (my ($id, $name, $bug_id, $value) = $sth->fetchrow_array()) { if ($excluded_bugs{$bug_id}) { $count++; next; } # Standard fields - my $data = {bug_id => $bug_id}; + my $data = {id => $id, bug_id => $bug_id}; # Fields that require custom values based on other criteria if (exists $private_bugs{$bug_id}) { @@ -501,6 +522,11 @@ sub process_keywords { my $count = 0; my $last_offset = 0; + if (check_duplicate_data($table_name)) { + logger("Skipping $table_name due to duplicate data"); + return; + } + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM keywords'); logger("Processing $total $table_name."); @@ -596,18 +622,27 @@ sub process_users { my $count = 0; my $last_offset = 0; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + # Retrieve the max ID from BQ in case we didn'complete last time + my $max_id = get_max_id($table_name); + + my $total + = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles WHERE userid > ?', + undef, $max_id); logger("Processing $total $table_name."); my $sth = $dbh->prepare( - 'SELECT userid, modification_ts FROM profiles ORDER BY userid LIMIT ? OFFSET ?' + 'SELECT userid, modification_ts FROM profiles WHERE userid > ? ORDER BY userid LIMIT ? OFFSET ?' ); + logger("max id: $max_id", DEBUG_OUTPUT); + while ($count < $total) { my @users = (); - $sth->execute(API_BLOCK_COUNT, $last_offset); + logger("last offset: $last_offset", DEBUG_OUTPUT); + + $sth->execute($max_id, API_BLOCK_COUNT, $last_offset); while (my ($id, $mod_time) = $sth->fetchrow_array()) { logger("Processing id $id with mod_time of $mod_time."); @@ -620,16 +655,18 @@ sub process_users { my $data = get_cache($id, $table_name, $mod_time); if (!$data) { - logger("$table_name id $id with time $mod_time not found in cache.", DEBUG_OUTPUT); + logger("$table_name id $id with time $mod_time not found in cache.", + DEBUG_OUTPUT); my $obj = Bugzilla::User->new($id); # Standard fields $data = { id => $obj->id, - last_seen => ($obj->last_seen_date ? $obj->last_seen_date . ' 00:00:00' : undef), - email => $obj->email, - is_new => ($obj->is_new ? true : false), + last_seen => + ($obj->last_seen_date ? $obj->last_seen_date . ' 00:00:00' : undef), + email => $obj->email, + is_new => ($obj->is_new ? true : false), }; # Fields that require custom values based on criteria @@ -667,6 +704,11 @@ sub process_two_columns { my $columns_string = join ', ', @{$column_names}; my $order_by = $column_names->[0]; + if (check_duplicate_data($bq_name)) { + logger("Skipping $table_name due to duplicate data"); + return; + } + my $sth = $dbh->prepare( "SELECT $columns_string FROM $table_name ORDER BY $order_by LIMIT ? OFFSET ?"); @@ -705,23 +747,23 @@ sub get_cache { return undef; } - logger("Retreiving data from $table for $id with time $timestamp.", DEBUG_OUTPUT); + logger("Retreiving data from $table for $id with time $timestamp.", + DEBUG_OUTPUT); try { - # Retrieve compressed JSON from cache table if it exists - my $gzipped_data = $dbh->selectrow_array( - 'SELECT data FROM bmo_etl_cache WHERE id = ? AND table_name = ? AND snapshot_date = ?', - undef, $id, $table, $timestamp - ); - return undef if !$gzipped_data; + # Retrieve compressed JSON from cache table if it exists + my $gzipped_data = $dbh->selectrow_array( + 'SELECT data FROM bmo_etl_cache WHERE id = ? AND table_name = ? AND snapshot_date = ?', + undef, $id, $table, $timestamp + ); + return undef if !$gzipped_data; - # First uncompress the JSON and then decode it back to Perl data - my $data; - unless (gunzip \$gzipped_data => \$data) { - delete_lock(); - die "gunzip failed: $GunzipError\n"; - } - return decode_json($data); + # First uncompress the JSON and then decode it back to Perl data + my $data; + unless (gunzip \$gzipped_data => \$data) { + die "gunzip failed: $GunzipError\n"; + } + return decode_json($data); } catch { # Log the failure and return undef @@ -746,34 +788,40 @@ sub store_cache { # Compress the JSON to save space in the DB my $gzipped_data; unless (gzip \$data => \$gzipped_data) { - delete_lock(); die "gzip failed: $GzipError\n"; } # We need to use the main DB for write operations - my $main_dbh = Bugzilla->dbh_main; - + my $dbh_main = Bugzilla->dbh_main; try { + $dbh_main->bz_start_transaction; + # Clean out outdated JSON - $main_dbh->do('DELETE FROM bmo_etl_cache WHERE id = ? AND table_name = ?', + $dbh_main->do('DELETE FROM bmo_etl_cache WHERE id = ? AND table_name = ?', undef, $id, $table); # Enter new cached JSON - $main_dbh->do( + $dbh_main->do( 'INSERT INTO bmo_etl_cache (id, table_name, snapshot_date, data) VALUES (?, ?, ?, ?)', undef, $id, $table, $timestamp, $gzipped_data ); + + $dbh_main->bz_commit_transaction; } catch { - # Log the failure - WARN("ERROR: Unable to store cache data in database: $_"); - } + $dbh_main->bz_rollback_transaction; + + # Log the failure and return undef + WARN("ERROR: Unable to store cached data into database: $_"); + return undef; + }; } sub send_data { my ($table, $all_rows, $current_count) = @_; - logger('Sending ' . scalar @{$all_rows} . " rows to table $table using BigQuery API"); + logger( + 'Sending ' . scalar @{$all_rows} . " rows to table $table using BigQuery API"); # Add the same snapshot date to every row sent foreach my $row (@{$all_rows}) { @@ -785,7 +833,7 @@ sub send_data { push @json_rows, {json => $row}; } - my $big_query = {rows => \@json_rows}; + my $query = {rows => \@json_rows}; if ($test) { my $filename @@ -797,55 +845,25 @@ sub send_data { logger("Writing data to $filename."); my $fh = path($filename)->open('>>'); - print $fh encode_json($big_query) . "\n"; + print $fh encode_json($query) . "\n"; unless (close $fh) { - delete_lock(); die "Could not close $filename: $!\n"; } return; } - my $http_headers = HTTP::Headers->new; - - # Do not attempt to get access token if running in test environment - if ($base_url !~ /^http:\/\/[^\/]+:9050/) { - my $access_token = _get_access_token(); - $http_headers->header(Authorization => 'Bearer ' . $access_token); - } - - my $full_path = sprintf 'projects/%s/datasets/%s/tables/%s/insertAll', - $project_id, $dataset_id, $table; - - logger("Sending to $base_url/$full_path", DEBUG_OUTPUT); - - my $request = HTTP::Request->new('POST', "$base_url/$full_path", $http_headers); - $request->header('Content-Type' => 'application/json'); - - logger('Encoding content into JSON.', DEBUG_OUTPUT); - - $request->content(encode_json($big_query)); - - logger('Sending request', DEBUG_OUTPUT); + my $path = sprintf 'projects/%s/datasets/%s/tables/%s/insertAll', $project_id, + $dataset_id, $table; - my $response = $ua->request($request); + my $result = call_big_query('POST', $path, $query); - logger($response->content, DEBUG_OUTPUT); - - my $result = decode_json($response->content); - - if (!$response->is_success - || (exists $result->{insertErrors} && @{$result->{insertErrors}})) - { - delete_lock(); - die "Google Big Query insert failure:\nRequest:\n" - . $request->content - . "\n\nResponse:\n" - . $response->content . "\n"; + if (exists $result->{insertErrors} && @{$result->{insertErrors}}) { + die "Google Big Query insert failure: " . encode_json($result); } } -sub _get_access_token { +sub get_access_token { state $access_token; # We should only need to get this once state $token_expiry; @@ -857,10 +875,10 @@ sub _get_access_token { return $access_token; } - # Google Kubernetes allows for the use of Workload Identity. This allows - # us to link two service accounts together and give special access for applications - # running under Kubernetes. We use the special access to get an OAuth2 access_token - # that can then be used for accessing the the Google API such as BigQuery. +# Google Kubernetes allows for the use of Workload Identity. This allows +# us to link two service accounts together and give special access for applications +# running under Kubernetes. We use the special access to get an OAuth2 access_token +# that can then be used for accessing the the Google API such as BigQuery. my $url = sprintf 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/%s/token', @@ -874,7 +892,6 @@ sub _get_access_token { my $res = $ua->request($request); if (!$res->is_success) { - delete_lock(); die 'Google access token failure: ' . $res->content . "\n"; } @@ -887,82 +904,71 @@ sub _get_access_token { return $access_token; } -# If a previous process is performing an export to BigQuery, then -# we must check the lock table and exit if true. -sub check_and_set_lock { - return if $test; # No need if just dumping test files - - logger('Checking for previous lock or setting new one', DEBUG_OUTPUT); +sub call_big_query { + my ($method, $path, $data) = @_; - my $dbh_main = Bugzilla->dbh_main; + logger("BigQuery request - method: $method, path: $path", DEBUG_OUTPUT); - # Clear out any locks that are greater than 24h old - $dbh_main->do('DELETE FROM bmo_etl_locked WHERE creation_ts < ' - . $dbh_main->sql_date_math('NOW()', '-', 24, 'HOUR')); + my $http_headers = HTTP::Headers->new; - # Now check for any pre-existing locks and do not proceed if one found - my $locked = $dbh_main->selectrow_array('SELECT COUNT(*) FROM bmo_etl_locked'); - if ($locked) { - die "Another process has set a lock. Exiting\n"; + # Do not attempt to get access token if running in test environment + if ($base_url !~ /^http:\/\/[^\/]+:9050/) { + my $access_token = get_access_token(); + $http_headers->header(Authorization => 'Bearer ' . $access_token); } - logger('Previous lock not found. Setting new one.', DEBUG_OUTPUT); - - $dbh_main->do('INSERT INTO bmo_etl_locked (value, creation_ts) VALUES (?, NOW())', undef, 'locked'); -} + my $request = HTTP::Request->new($method, "$base_url/$path", $http_headers); + $request->header('Content-Type' => 'application/json'); -# Delete lock from bmo_etl_locked -sub delete_lock { - logger("Deleting lock in database."); - Bugzilla->dbh_main->do('DELETE FROM bmo_etl_locked'); -} + logger('Encoding content into JSON.', DEBUG_OUTPUT); + logger(encode_json($data), DEBUG_OUTPUT); + $request->content(encode_json($data)); -sub check_for_duplicates { - return if $test; # no need if just dumping test files + my $res = $ua->request($request); + logger($res->content, DEBUG_OUTPUT); - logger("Checking for duplicate data for snapshot date $snapshot_date."); + if (!$res->is_success) { + die 'Google Big Query query failure: ' . $res->content . "\n"; + } - my $http_headers = HTTP::Headers->new; + my $result = decode_json($res->content); +} - # Do not attempt to get access token if running in test environment - if ($base_url !~ /^http:\/\/[^\/]+:9050/) { - my $access_token = _get_access_token(); - $http_headers->header(Authorization => 'Bearer ' . $access_token); - } +sub get_max_id { + my ($table) = @_; - my $full_path = "projects/$project_id/queries"; + return 0 if $test; # no need if just dumping test files - logger("Querying $base_url/$full_path", DEBUG_OUTPUT); + logger("Retrieving max id for table $table for snapshot date $snapshot_date."); my $query = { query => - "SELECT count(*) FROM ${project_id}.${dataset_id}.bugs WHERE snapshot_date = '$snapshot_date';", - useLegacySql => false, + "SELECT max(id) FROM ${project_id}.${dataset_id}.${table} WHERE snapshot_date = '$snapshot_date';", + useLegacySql => false }; - my $request = HTTP::Request->new('POST', "$base_url/$full_path", $http_headers); - $request->header('Content-Type' => 'application/json'); - $request->content(encode_json($query)); + my $result = call_big_query('POST', "projects/$project_id/queries", $query); - logger(encode_json($query), DEBUG_OUTPUT); + return $result->{rows}->[0]->{f}->[0]->{v} || 0; +} - my $res = $ua->request($request); - if (!$res->is_success) { - delete_lock(); - die 'Google Big Query query failure: ' . $res->content . "\n"; - } +sub check_duplicate_data { + my ($table) = @_; - logger($res->content, DEBUG_OUTPUT); + return 0 if $test; # no need if just dumping test files - my $result = decode_json($res->content); + logger( + "Checking duplicate data for table $table for snapshot date $snapshot_date."); - my $row_count = $result->{rows}->[0]->{f}->[0]->{v}; + my $query = { + query => + "SELECT count(*) FROM ${project_id}.${dataset_id}.${table} WHERE snapshot_date = '$snapshot_date';", + useLegacySql => false + }; - # Do not export if we have any rows with this snapshot date. - if ($row_count) { - delete_lock(); - die "Duplicate data found for snapshot date $snapshot_date\n"; - } + my $result = call_big_query('POST', "projects/$project_id/queries", $query); + + return $result->{rows}->[0]->{f}->[0]->{v} || 0; } sub get_multi_group_value { diff --git a/extensions/BMO/t/bmo/bmo_etl.t b/extensions/BMO/t/bmo/bmo_etl.t index 0a7a1234f8..b9159176c9 100644 --- a/extensions/BMO/t/bmo/bmo_etl.t +++ b/extensions/BMO/t/bmo/bmo_etl.t @@ -199,14 +199,39 @@ $t->post_ok( 'http://bq:9050/bigquery/v2/projects/test/queries' => json => $query) ->status_is(200)->json_is('/rows/0/f/0/v' => $bug_id_1); -### Section 7: Exporting again on the same day (with the same snapshot date) will cause the script to exit +### Section: 7 - Add an additional bug and then run the export script again +# Run with the same timestamp to make sure it adds the bug. +# Simulate a script stop and resume condition +$t->post_ok($url + . 'rest/bug' => {'X-Bugzilla-API-Key' => $admin_api_key} => json => + $new_bug_1)->status_is(200)->json_has('/id'); + +my $bug_id_3 = $t->tx->res->json->{id}; + +$t->post_ok($url + . "rest/bug/$bug_id_3/attachment" => + {'X-Bugzilla-API-Key' => $admin_api_key} => json => $new_attach_1) + ->status_is(201)->json_has('/attachments'); + +($attach_id) = keys %{$t->tx->res->json->{attachments}}; @cmd = ( './extensions/BMO/bin/export_bmo_etl.pl', - '--debug', '--snapshot-date', $snapshot_date, + '--snapshot-date', $snapshot_date, ); ($output, $error, $rv) = capture { system @cmd; }; -ok($rv, 'Duplicate data exported to BigQuery test instance should fail'); +ok(!$rv, 'Data exported to BigQuery test instance without error'); + +$query = { + query => 'SELECT summary FROM test.bugzilla.bugs WHERE id = ' + . $bug_id_3 + . ' AND snapshot_date = \'' + . $snapshot_date . '\';', + useLegacySql => false +}; +$t->post_ok( + 'http://bq:9050/bigquery/v2/projects/test/queries' => json => $query) + ->status_is(200)->json_is('/rows/0/f/0/v' => $new_bug_1->{summary}); done_testing;