You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

409 lines
11 KiB

  1. // Copyright 2015 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package bigquery
  15. import (
  16. "testing"
  17. "time"
  18. "cloud.google.com/go/internal/testutil"
  19. "github.com/google/go-cmp/cmp"
  20. bq "google.golang.org/api/bigquery/v2"
  21. )
  22. func defaultQueryJob() *bq.Job {
  23. pfalse := false
  24. return &bq.Job{
  25. JobReference: &bq.JobReference{JobId: "RANDOM", ProjectId: "client-project-id"},
  26. Configuration: &bq.JobConfiguration{
  27. Query: &bq.JobConfigurationQuery{
  28. DestinationTable: &bq.TableReference{
  29. ProjectId: "client-project-id",
  30. DatasetId: "dataset-id",
  31. TableId: "table-id",
  32. },
  33. Query: "query string",
  34. DefaultDataset: &bq.DatasetReference{
  35. ProjectId: "def-project-id",
  36. DatasetId: "def-dataset-id",
  37. },
  38. UseLegacySql: &pfalse,
  39. },
  40. },
  41. }
  42. }
  43. var defaultQuery = &QueryConfig{
  44. Q: "query string",
  45. DefaultProjectID: "def-project-id",
  46. DefaultDatasetID: "def-dataset-id",
  47. }
  48. func TestQuery(t *testing.T) {
  49. defer fixRandomID("RANDOM")()
  50. c := &Client{
  51. projectID: "client-project-id",
  52. }
  53. testCases := []struct {
  54. dst *Table
  55. src *QueryConfig
  56. jobIDConfig JobIDConfig
  57. want *bq.Job
  58. }{
  59. {
  60. dst: c.Dataset("dataset-id").Table("table-id"),
  61. src: defaultQuery,
  62. want: defaultQueryJob(),
  63. },
  64. {
  65. dst: c.Dataset("dataset-id").Table("table-id"),
  66. src: &QueryConfig{
  67. Q: "query string",
  68. Labels: map[string]string{"a": "b"},
  69. DryRun: true,
  70. },
  71. want: func() *bq.Job {
  72. j := defaultQueryJob()
  73. j.Configuration.Labels = map[string]string{"a": "b"}
  74. j.Configuration.DryRun = true
  75. j.Configuration.Query.DefaultDataset = nil
  76. return j
  77. }(),
  78. },
  79. {
  80. dst: c.Dataset("dataset-id").Table("table-id"),
  81. jobIDConfig: JobIDConfig{JobID: "jobID", AddJobIDSuffix: true},
  82. src: &QueryConfig{Q: "query string"},
  83. want: func() *bq.Job {
  84. j := defaultQueryJob()
  85. j.Configuration.Query.DefaultDataset = nil
  86. j.JobReference.JobId = "jobID-RANDOM"
  87. return j
  88. }(),
  89. },
  90. {
  91. dst: &Table{},
  92. src: defaultQuery,
  93. want: func() *bq.Job {
  94. j := defaultQueryJob()
  95. j.Configuration.Query.DestinationTable = nil
  96. return j
  97. }(),
  98. },
  99. {
  100. dst: c.Dataset("dataset-id").Table("table-id"),
  101. src: &QueryConfig{
  102. Q: "query string",
  103. TableDefinitions: map[string]ExternalData{
  104. "atable": func() *GCSReference {
  105. g := NewGCSReference("uri")
  106. g.AllowJaggedRows = true
  107. g.AllowQuotedNewlines = true
  108. g.Compression = Gzip
  109. g.Encoding = UTF_8
  110. g.FieldDelimiter = ";"
  111. g.IgnoreUnknownValues = true
  112. g.MaxBadRecords = 1
  113. g.Quote = "'"
  114. g.SkipLeadingRows = 2
  115. g.Schema = Schema{{Name: "name", Type: StringFieldType}}
  116. return g
  117. }(),
  118. },
  119. },
  120. want: func() *bq.Job {
  121. j := defaultQueryJob()
  122. j.Configuration.Query.DefaultDataset = nil
  123. td := make(map[string]bq.ExternalDataConfiguration)
  124. quote := "'"
  125. td["atable"] = bq.ExternalDataConfiguration{
  126. Compression: "GZIP",
  127. IgnoreUnknownValues: true,
  128. MaxBadRecords: 1,
  129. SourceFormat: "CSV", // must be explicitly set.
  130. SourceUris: []string{"uri"},
  131. CsvOptions: &bq.CsvOptions{
  132. AllowJaggedRows: true,
  133. AllowQuotedNewlines: true,
  134. Encoding: "UTF-8",
  135. FieldDelimiter: ";",
  136. SkipLeadingRows: 2,
  137. Quote: &quote,
  138. },
  139. Schema: &bq.TableSchema{
  140. Fields: []*bq.TableFieldSchema{
  141. {Name: "name", Type: "STRING"},
  142. },
  143. },
  144. }
  145. j.Configuration.Query.TableDefinitions = td
  146. return j
  147. }(),
  148. },
  149. {
  150. dst: &Table{
  151. ProjectID: "project-id",
  152. DatasetID: "dataset-id",
  153. TableID: "table-id",
  154. },
  155. src: &QueryConfig{
  156. Q: "query string",
  157. DefaultProjectID: "def-project-id",
  158. DefaultDatasetID: "def-dataset-id",
  159. CreateDisposition: CreateNever,
  160. WriteDisposition: WriteTruncate,
  161. },
  162. want: func() *bq.Job {
  163. j := defaultQueryJob()
  164. j.Configuration.Query.DestinationTable.ProjectId = "project-id"
  165. j.Configuration.Query.WriteDisposition = "WRITE_TRUNCATE"
  166. j.Configuration.Query.CreateDisposition = "CREATE_NEVER"
  167. return j
  168. }(),
  169. },
  170. {
  171. dst: c.Dataset("dataset-id").Table("table-id"),
  172. src: &QueryConfig{
  173. Q: "query string",
  174. DefaultProjectID: "def-project-id",
  175. DefaultDatasetID: "def-dataset-id",
  176. DisableQueryCache: true,
  177. },
  178. want: func() *bq.Job {
  179. j := defaultQueryJob()
  180. f := false
  181. j.Configuration.Query.UseQueryCache = &f
  182. return j
  183. }(),
  184. },
  185. {
  186. dst: c.Dataset("dataset-id").Table("table-id"),
  187. src: &QueryConfig{
  188. Q: "query string",
  189. DefaultProjectID: "def-project-id",
  190. DefaultDatasetID: "def-dataset-id",
  191. AllowLargeResults: true,
  192. },
  193. want: func() *bq.Job {
  194. j := defaultQueryJob()
  195. j.Configuration.Query.AllowLargeResults = true
  196. return j
  197. }(),
  198. },
  199. {
  200. dst: c.Dataset("dataset-id").Table("table-id"),
  201. src: &QueryConfig{
  202. Q: "query string",
  203. DefaultProjectID: "def-project-id",
  204. DefaultDatasetID: "def-dataset-id",
  205. DisableFlattenedResults: true,
  206. },
  207. want: func() *bq.Job {
  208. j := defaultQueryJob()
  209. f := false
  210. j.Configuration.Query.FlattenResults = &f
  211. j.Configuration.Query.AllowLargeResults = true
  212. return j
  213. }(),
  214. },
  215. {
  216. dst: c.Dataset("dataset-id").Table("table-id"),
  217. src: &QueryConfig{
  218. Q: "query string",
  219. DefaultProjectID: "def-project-id",
  220. DefaultDatasetID: "def-dataset-id",
  221. Priority: QueryPriority("low"),
  222. },
  223. want: func() *bq.Job {
  224. j := defaultQueryJob()
  225. j.Configuration.Query.Priority = "low"
  226. return j
  227. }(),
  228. },
  229. {
  230. dst: c.Dataset("dataset-id").Table("table-id"),
  231. src: &QueryConfig{
  232. Q: "query string",
  233. DefaultProjectID: "def-project-id",
  234. DefaultDatasetID: "def-dataset-id",
  235. MaxBillingTier: 3,
  236. MaxBytesBilled: 5,
  237. },
  238. want: func() *bq.Job {
  239. j := defaultQueryJob()
  240. tier := int64(3)
  241. j.Configuration.Query.MaximumBillingTier = &tier
  242. j.Configuration.Query.MaximumBytesBilled = 5
  243. return j
  244. }(),
  245. },
  246. {
  247. dst: c.Dataset("dataset-id").Table("table-id"),
  248. src: &QueryConfig{
  249. Q: "query string",
  250. DefaultProjectID: "def-project-id",
  251. DefaultDatasetID: "def-dataset-id",
  252. UseStandardSQL: true,
  253. },
  254. want: defaultQueryJob(),
  255. },
  256. {
  257. dst: c.Dataset("dataset-id").Table("table-id"),
  258. src: &QueryConfig{
  259. Q: "query string",
  260. DefaultProjectID: "def-project-id",
  261. DefaultDatasetID: "def-dataset-id",
  262. UseLegacySQL: true,
  263. },
  264. want: func() *bq.Job {
  265. j := defaultQueryJob()
  266. ptrue := true
  267. j.Configuration.Query.UseLegacySql = &ptrue
  268. j.Configuration.Query.ForceSendFields = nil
  269. return j
  270. }(),
  271. },
  272. }
  273. for i, tc := range testCases {
  274. query := c.Query("")
  275. query.JobIDConfig = tc.jobIDConfig
  276. query.QueryConfig = *tc.src
  277. query.Dst = tc.dst
  278. got, err := query.newJob()
  279. if err != nil {
  280. t.Errorf("#%d: err calling query: %v", i, err)
  281. continue
  282. }
  283. checkJob(t, i, got, tc.want)
  284. // Round-trip.
  285. jc, err := bqToJobConfig(got.Configuration, c)
  286. if err != nil {
  287. t.Fatalf("#%d: %v", i, err)
  288. }
  289. wantConfig := query.QueryConfig
  290. // We set AllowLargeResults to true when DisableFlattenedResults is true.
  291. if wantConfig.DisableFlattenedResults {
  292. wantConfig.AllowLargeResults = true
  293. }
  294. // A QueryConfig with neither UseXXXSQL field set is equivalent
  295. // to one where UseStandardSQL = true.
  296. if !wantConfig.UseLegacySQL && !wantConfig.UseStandardSQL {
  297. wantConfig.UseStandardSQL = true
  298. }
  299. // Treat nil and empty tables the same, and ignore the client.
  300. tableEqual := func(t1, t2 *Table) bool {
  301. if t1 == nil {
  302. t1 = &Table{}
  303. }
  304. if t2 == nil {
  305. t2 = &Table{}
  306. }
  307. return t1.ProjectID == t2.ProjectID && t1.DatasetID == t2.DatasetID && t1.TableID == t2.TableID
  308. }
  309. // A table definition that is a GCSReference round-trips as an ExternalDataConfig.
  310. // TODO(jba): see if there is a way to express this with a transformer.
  311. gcsRefToEDC := func(g *GCSReference) *ExternalDataConfig {
  312. q := g.toBQ()
  313. e, _ := bqToExternalDataConfig(&q)
  314. return e
  315. }
  316. externalDataEqual := func(e1, e2 ExternalData) bool {
  317. if r, ok := e1.(*GCSReference); ok {
  318. e1 = gcsRefToEDC(r)
  319. }
  320. if r, ok := e2.(*GCSReference); ok {
  321. e2 = gcsRefToEDC(r)
  322. }
  323. return cmp.Equal(e1, e2)
  324. }
  325. diff := testutil.Diff(jc.(*QueryConfig), &wantConfig,
  326. cmp.Comparer(tableEqual),
  327. cmp.Comparer(externalDataEqual),
  328. )
  329. if diff != "" {
  330. t.Errorf("#%d: (got=-, want=+:\n%s", i, diff)
  331. }
  332. }
  333. }
  334. func TestConfiguringQuery(t *testing.T) {
  335. c := &Client{
  336. projectID: "project-id",
  337. }
  338. query := c.Query("q")
  339. query.JobID = "ajob"
  340. query.DefaultProjectID = "def-project-id"
  341. query.DefaultDatasetID = "def-dataset-id"
  342. query.TimePartitioning = &TimePartitioning{Expiration: 1234 * time.Second, Field: "f"}
  343. query.Clustering = &Clustering{
  344. Fields: []string{"cfield1"},
  345. }
  346. query.DestinationEncryptionConfig = &EncryptionConfig{KMSKeyName: "keyName"}
  347. query.SchemaUpdateOptions = []string{"ALLOW_FIELD_ADDITION"}
  348. // Note: Other configuration fields are tested in other tests above.
  349. // A lot of that can be consolidated once Client.Copy is gone.
  350. pfalse := false
  351. want := &bq.Job{
  352. Configuration: &bq.JobConfiguration{
  353. Query: &bq.JobConfigurationQuery{
  354. Query: "q",
  355. DefaultDataset: &bq.DatasetReference{
  356. ProjectId: "def-project-id",
  357. DatasetId: "def-dataset-id",
  358. },
  359. UseLegacySql: &pfalse,
  360. TimePartitioning: &bq.TimePartitioning{ExpirationMs: 1234000, Field: "f", Type: "DAY"},
  361. Clustering: &bq.Clustering{Fields: []string{"cfield1"}},
  362. DestinationEncryptionConfiguration: &bq.EncryptionConfiguration{KmsKeyName: "keyName"},
  363. SchemaUpdateOptions: []string{"ALLOW_FIELD_ADDITION"},
  364. },
  365. },
  366. JobReference: &bq.JobReference{
  367. JobId: "ajob",
  368. ProjectId: "project-id",
  369. },
  370. }
  371. got, err := query.newJob()
  372. if err != nil {
  373. t.Fatalf("err calling Query.newJob: %v", err)
  374. }
  375. if diff := testutil.Diff(got, want); diff != "" {
  376. t.Errorf("querying: -got +want:\n%s", diff)
  377. }
  378. }
  379. func TestQueryLegacySQL(t *testing.T) {
  380. c := &Client{projectID: "project-id"}
  381. q := c.Query("q")
  382. q.UseStandardSQL = true
  383. q.UseLegacySQL = true
  384. _, err := q.newJob()
  385. if err == nil {
  386. t.Error("UseStandardSQL and UseLegacySQL: got nil, want error")
  387. }
  388. q = c.Query("q")
  389. q.Parameters = []QueryParameter{{Name: "p", Value: 3}}
  390. q.UseLegacySQL = true
  391. _, err = q.newJob()
  392. if err == nil {
  393. t.Error("Parameters and UseLegacySQL: got nil, want error")
  394. }
  395. }