table.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  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. "context"
  17. "errors"
  18. "fmt"
  19. "time"
  20. "cloud.google.com/go/internal/optional"
  21. "cloud.google.com/go/internal/trace"
  22. bq "google.golang.org/api/bigquery/v2"
  23. )
  24. // A Table is a reference to a BigQuery table.
  25. type Table struct {
  26. // ProjectID, DatasetID and TableID may be omitted if the Table is the destination for a query.
  27. // In this case the result will be stored in an ephemeral table.
  28. ProjectID string
  29. DatasetID string
  30. // TableID must contain only letters (a-z, A-Z), numbers (0-9), or underscores (_).
  31. // The maximum length is 1,024 characters.
  32. TableID string
  33. c *Client
  34. }
  35. // TableMetadata contains information about a BigQuery table.
  36. type TableMetadata struct {
  37. // The following fields can be set when creating a table.
  38. // The user-friendly name for the table.
  39. Name string
  40. // The user-friendly description of the table.
  41. Description string
  42. // The table schema. If provided on create, ViewQuery must be empty.
  43. Schema Schema
  44. // The query to use for a view. If provided on create, Schema must be nil.
  45. ViewQuery string
  46. // Use Legacy SQL for the view query.
  47. // At most one of UseLegacySQL and UseStandardSQL can be true.
  48. UseLegacySQL bool
  49. // Use Legacy SQL for the view query. The default.
  50. // At most one of UseLegacySQL and UseStandardSQL can be true.
  51. // Deprecated: use UseLegacySQL.
  52. UseStandardSQL bool
  53. // If non-nil, the table is partitioned by time.
  54. TimePartitioning *TimePartitioning
  55. // Clustering specifies the data clustering configuration for the table.
  56. Clustering *Clustering
  57. // The time when this table expires. If set, this table will expire at the
  58. // specified time. Expired tables will be deleted and their storage
  59. // reclaimed. The zero value is ignored.
  60. ExpirationTime time.Time
  61. // User-provided labels.
  62. Labels map[string]string
  63. // Information about a table stored outside of BigQuery.
  64. ExternalDataConfig *ExternalDataConfig
  65. // Custom encryption configuration (e.g., Cloud KMS keys).
  66. EncryptionConfig *EncryptionConfig
  67. // All the fields below are read-only.
  68. FullID string // An opaque ID uniquely identifying the table.
  69. Type TableType
  70. CreationTime time.Time
  71. LastModifiedTime time.Time
  72. // The size of the table in bytes.
  73. // This does not include data that is being buffered during a streaming insert.
  74. NumBytes int64
  75. // The number of rows of data in this table.
  76. // This does not include data that is being buffered during a streaming insert.
  77. NumRows uint64
  78. // Contains information regarding this table's streaming buffer, if one is
  79. // present. This field will be nil if the table is not being streamed to or if
  80. // there is no data in the streaming buffer.
  81. StreamingBuffer *StreamingBuffer
  82. // ETag is the ETag obtained when reading metadata. Pass it to Table.Update to
  83. // ensure that the metadata hasn't changed since it was read.
  84. ETag string
  85. }
  86. // TableCreateDisposition specifies the circumstances under which destination table will be created.
  87. // Default is CreateIfNeeded.
  88. type TableCreateDisposition string
  89. const (
  90. // CreateIfNeeded will create the table if it does not already exist.
  91. // Tables are created atomically on successful completion of a job.
  92. CreateIfNeeded TableCreateDisposition = "CREATE_IF_NEEDED"
  93. // CreateNever ensures the table must already exist and will not be
  94. // automatically created.
  95. CreateNever TableCreateDisposition = "CREATE_NEVER"
  96. )
  97. // TableWriteDisposition specifies how existing data in a destination table is treated.
  98. // Default is WriteAppend.
  99. type TableWriteDisposition string
  100. const (
  101. // WriteAppend will append to any existing data in the destination table.
  102. // Data is appended atomically on successful completion of a job.
  103. WriteAppend TableWriteDisposition = "WRITE_APPEND"
  104. // WriteTruncate overrides the existing data in the destination table.
  105. // Data is overwritten atomically on successful completion of a job.
  106. WriteTruncate TableWriteDisposition = "WRITE_TRUNCATE"
  107. // WriteEmpty fails writes if the destination table already contains data.
  108. WriteEmpty TableWriteDisposition = "WRITE_EMPTY"
  109. )
  110. // TableType is the type of table.
  111. type TableType string
  112. const (
  113. // RegularTable is a regular table.
  114. RegularTable TableType = "TABLE"
  115. // ViewTable is a table type describing that the table is view. See more
  116. // information at https://cloud.google.com/bigquery/docs/views.
  117. ViewTable TableType = "VIEW"
  118. // ExternalTable is a table type describing that the table is an external
  119. // table (also known as a federated data source). See more information at
  120. // https://cloud.google.com/bigquery/external-data-sources.
  121. ExternalTable TableType = "EXTERNAL"
  122. )
  123. // TimePartitioning describes the time-based date partitioning on a table.
  124. // For more information see: https://cloud.google.com/bigquery/docs/creating-partitioned-tables.
  125. type TimePartitioning struct {
  126. // The amount of time to keep the storage for a partition.
  127. // If the duration is empty (0), the data in the partitions do not expire.
  128. Expiration time.Duration
  129. // If empty, the table is partitioned by pseudo column '_PARTITIONTIME'; if set, the
  130. // table is partitioned by this field. The field must be a top-level TIMESTAMP or
  131. // DATE field. Its mode must be NULLABLE or REQUIRED.
  132. Field string
  133. // If true, queries that reference this table must include a filter (e.g. a WHERE predicate)
  134. // that can be used for partition elimination.
  135. RequirePartitionFilter bool
  136. }
  137. func (p *TimePartitioning) toBQ() *bq.TimePartitioning {
  138. if p == nil {
  139. return nil
  140. }
  141. return &bq.TimePartitioning{
  142. Type: "DAY",
  143. ExpirationMs: int64(p.Expiration / time.Millisecond),
  144. Field: p.Field,
  145. RequirePartitionFilter: p.RequirePartitionFilter,
  146. }
  147. }
  148. func bqToTimePartitioning(q *bq.TimePartitioning) *TimePartitioning {
  149. if q == nil {
  150. return nil
  151. }
  152. return &TimePartitioning{
  153. Expiration: time.Duration(q.ExpirationMs) * time.Millisecond,
  154. Field: q.Field,
  155. RequirePartitionFilter: q.RequirePartitionFilter,
  156. }
  157. }
  158. // Clustering governs the organization of data within a partitioned table.
  159. // For more information, see https://cloud.google.com/bigquery/docs/clustered-tables
  160. type Clustering struct {
  161. Fields []string
  162. }
  163. func (c *Clustering) toBQ() *bq.Clustering {
  164. if c == nil {
  165. return nil
  166. }
  167. return &bq.Clustering{
  168. Fields: c.Fields,
  169. }
  170. }
  171. func bqToClustering(q *bq.Clustering) *Clustering {
  172. if q == nil {
  173. return nil
  174. }
  175. return &Clustering{
  176. Fields: q.Fields,
  177. }
  178. }
  179. // EncryptionConfig configures customer-managed encryption on tables.
  180. type EncryptionConfig struct {
  181. // Describes the Cloud KMS encryption key that will be used to protect
  182. // destination BigQuery table. The BigQuery Service Account associated with your
  183. // project requires access to this encryption key.
  184. KMSKeyName string
  185. }
  186. func (e *EncryptionConfig) toBQ() *bq.EncryptionConfiguration {
  187. if e == nil {
  188. return nil
  189. }
  190. return &bq.EncryptionConfiguration{
  191. KmsKeyName: e.KMSKeyName,
  192. }
  193. }
  194. func bqToEncryptionConfig(q *bq.EncryptionConfiguration) *EncryptionConfig {
  195. if q == nil {
  196. return nil
  197. }
  198. return &EncryptionConfig{
  199. KMSKeyName: q.KmsKeyName,
  200. }
  201. }
  202. // StreamingBuffer holds information about the streaming buffer.
  203. type StreamingBuffer struct {
  204. // A lower-bound estimate of the number of bytes currently in the streaming
  205. // buffer.
  206. EstimatedBytes uint64
  207. // A lower-bound estimate of the number of rows currently in the streaming
  208. // buffer.
  209. EstimatedRows uint64
  210. // The time of the oldest entry in the streaming buffer.
  211. OldestEntryTime time.Time
  212. }
  213. func (t *Table) toBQ() *bq.TableReference {
  214. return &bq.TableReference{
  215. ProjectId: t.ProjectID,
  216. DatasetId: t.DatasetID,
  217. TableId: t.TableID,
  218. }
  219. }
  220. // FullyQualifiedName returns the ID of the table in projectID:datasetID.tableID format.
  221. func (t *Table) FullyQualifiedName() string {
  222. return fmt.Sprintf("%s:%s.%s", t.ProjectID, t.DatasetID, t.TableID)
  223. }
  224. // implicitTable reports whether Table is an empty placeholder, which signifies that a new table should be created with an auto-generated Table ID.
  225. func (t *Table) implicitTable() bool {
  226. return t.ProjectID == "" && t.DatasetID == "" && t.TableID == ""
  227. }
  228. // Create creates a table in the BigQuery service.
  229. // Pass in a TableMetadata value to configure the table.
  230. // If tm.View.Query is non-empty, the created table will be of type VIEW.
  231. // If no ExpirationTime is specified, the table will never expire.
  232. // After table creation, a view can be modified only if its table was initially created
  233. // with a view.
  234. func (t *Table) Create(ctx context.Context, tm *TableMetadata) (err error) {
  235. ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Create")
  236. defer func() { trace.EndSpan(ctx, err) }()
  237. table, err := tm.toBQ()
  238. if err != nil {
  239. return err
  240. }
  241. table.TableReference = &bq.TableReference{
  242. ProjectId: t.ProjectID,
  243. DatasetId: t.DatasetID,
  244. TableId: t.TableID,
  245. }
  246. req := t.c.bqs.Tables.Insert(t.ProjectID, t.DatasetID, table).Context(ctx)
  247. setClientHeader(req.Header())
  248. _, err = req.Do()
  249. return err
  250. }
  251. func (tm *TableMetadata) toBQ() (*bq.Table, error) {
  252. t := &bq.Table{}
  253. if tm == nil {
  254. return t, nil
  255. }
  256. if tm.Schema != nil && tm.ViewQuery != "" {
  257. return nil, errors.New("bigquery: provide Schema or ViewQuery, not both")
  258. }
  259. t.FriendlyName = tm.Name
  260. t.Description = tm.Description
  261. t.Labels = tm.Labels
  262. if tm.Schema != nil {
  263. t.Schema = tm.Schema.toBQ()
  264. }
  265. if tm.ViewQuery != "" {
  266. if tm.UseStandardSQL && tm.UseLegacySQL {
  267. return nil, errors.New("bigquery: cannot provide both UseStandardSQL and UseLegacySQL")
  268. }
  269. t.View = &bq.ViewDefinition{Query: tm.ViewQuery}
  270. if tm.UseLegacySQL {
  271. t.View.UseLegacySql = true
  272. } else {
  273. t.View.UseLegacySql = false
  274. t.View.ForceSendFields = append(t.View.ForceSendFields, "UseLegacySql")
  275. }
  276. } else if tm.UseLegacySQL || tm.UseStandardSQL {
  277. return nil, errors.New("bigquery: UseLegacy/StandardSQL requires ViewQuery")
  278. }
  279. t.TimePartitioning = tm.TimePartitioning.toBQ()
  280. t.Clustering = tm.Clustering.toBQ()
  281. if !validExpiration(tm.ExpirationTime) {
  282. return nil, fmt.Errorf("invalid expiration time: %v.\n"+
  283. "Valid expiration times are after 1678 and before 2262", tm.ExpirationTime)
  284. }
  285. if !tm.ExpirationTime.IsZero() && tm.ExpirationTime != NeverExpire {
  286. t.ExpirationTime = tm.ExpirationTime.UnixNano() / 1e6
  287. }
  288. if tm.ExternalDataConfig != nil {
  289. edc := tm.ExternalDataConfig.toBQ()
  290. t.ExternalDataConfiguration = &edc
  291. }
  292. t.EncryptionConfiguration = tm.EncryptionConfig.toBQ()
  293. if tm.FullID != "" {
  294. return nil, errors.New("cannot set FullID on create")
  295. }
  296. if tm.Type != "" {
  297. return nil, errors.New("cannot set Type on create")
  298. }
  299. if !tm.CreationTime.IsZero() {
  300. return nil, errors.New("cannot set CreationTime on create")
  301. }
  302. if !tm.LastModifiedTime.IsZero() {
  303. return nil, errors.New("cannot set LastModifiedTime on create")
  304. }
  305. if tm.NumBytes != 0 {
  306. return nil, errors.New("cannot set NumBytes on create")
  307. }
  308. if tm.NumRows != 0 {
  309. return nil, errors.New("cannot set NumRows on create")
  310. }
  311. if tm.StreamingBuffer != nil {
  312. return nil, errors.New("cannot set StreamingBuffer on create")
  313. }
  314. if tm.ETag != "" {
  315. return nil, errors.New("cannot set ETag on create")
  316. }
  317. return t, nil
  318. }
  319. // Metadata fetches the metadata for the table.
  320. func (t *Table) Metadata(ctx context.Context) (md *TableMetadata, err error) {
  321. ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Metadata")
  322. defer func() { trace.EndSpan(ctx, err) }()
  323. req := t.c.bqs.Tables.Get(t.ProjectID, t.DatasetID, t.TableID).Context(ctx)
  324. setClientHeader(req.Header())
  325. var table *bq.Table
  326. err = runWithRetry(ctx, func() (err error) {
  327. table, err = req.Do()
  328. return err
  329. })
  330. if err != nil {
  331. return nil, err
  332. }
  333. return bqToTableMetadata(table)
  334. }
  335. func bqToTableMetadata(t *bq.Table) (*TableMetadata, error) {
  336. md := &TableMetadata{
  337. Description: t.Description,
  338. Name: t.FriendlyName,
  339. Type: TableType(t.Type),
  340. FullID: t.Id,
  341. Labels: t.Labels,
  342. NumBytes: t.NumBytes,
  343. NumRows: t.NumRows,
  344. ExpirationTime: unixMillisToTime(t.ExpirationTime),
  345. CreationTime: unixMillisToTime(t.CreationTime),
  346. LastModifiedTime: unixMillisToTime(int64(t.LastModifiedTime)),
  347. ETag: t.Etag,
  348. EncryptionConfig: bqToEncryptionConfig(t.EncryptionConfiguration),
  349. }
  350. if t.Schema != nil {
  351. md.Schema = bqToSchema(t.Schema)
  352. }
  353. if t.View != nil {
  354. md.ViewQuery = t.View.Query
  355. md.UseLegacySQL = t.View.UseLegacySql
  356. }
  357. md.TimePartitioning = bqToTimePartitioning(t.TimePartitioning)
  358. md.Clustering = bqToClustering(t.Clustering)
  359. if t.StreamingBuffer != nil {
  360. md.StreamingBuffer = &StreamingBuffer{
  361. EstimatedBytes: t.StreamingBuffer.EstimatedBytes,
  362. EstimatedRows: t.StreamingBuffer.EstimatedRows,
  363. OldestEntryTime: unixMillisToTime(int64(t.StreamingBuffer.OldestEntryTime)),
  364. }
  365. }
  366. if t.ExternalDataConfiguration != nil {
  367. edc, err := bqToExternalDataConfig(t.ExternalDataConfiguration)
  368. if err != nil {
  369. return nil, err
  370. }
  371. md.ExternalDataConfig = edc
  372. }
  373. return md, nil
  374. }
  375. // Delete deletes the table.
  376. func (t *Table) Delete(ctx context.Context) (err error) {
  377. ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Delete")
  378. defer func() { trace.EndSpan(ctx, err) }()
  379. req := t.c.bqs.Tables.Delete(t.ProjectID, t.DatasetID, t.TableID).Context(ctx)
  380. setClientHeader(req.Header())
  381. return req.Do()
  382. }
  383. // Read fetches the contents of the table.
  384. func (t *Table) Read(ctx context.Context) *RowIterator {
  385. return t.read(ctx, fetchPage)
  386. }
  387. func (t *Table) read(ctx context.Context, pf pageFetcher) *RowIterator {
  388. return newRowIterator(ctx, t, pf)
  389. }
  390. // NeverExpire is a sentinel value used to remove a table'e expiration time.
  391. var NeverExpire = time.Time{}.Add(-1)
  392. // Update modifies specific Table metadata fields.
  393. func (t *Table) Update(ctx context.Context, tm TableMetadataToUpdate, etag string) (md *TableMetadata, err error) {
  394. ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Update")
  395. defer func() { trace.EndSpan(ctx, err) }()
  396. bqt, err := tm.toBQ()
  397. if err != nil {
  398. return nil, err
  399. }
  400. call := t.c.bqs.Tables.Patch(t.ProjectID, t.DatasetID, t.TableID, bqt).Context(ctx)
  401. setClientHeader(call.Header())
  402. if etag != "" {
  403. call.Header().Set("If-Match", etag)
  404. }
  405. var res *bq.Table
  406. if err := runWithRetry(ctx, func() (err error) {
  407. res, err = call.Do()
  408. return err
  409. }); err != nil {
  410. return nil, err
  411. }
  412. return bqToTableMetadata(res)
  413. }
  414. func (tm *TableMetadataToUpdate) toBQ() (*bq.Table, error) {
  415. t := &bq.Table{}
  416. forceSend := func(field string) {
  417. t.ForceSendFields = append(t.ForceSendFields, field)
  418. }
  419. if tm.Description != nil {
  420. t.Description = optional.ToString(tm.Description)
  421. forceSend("Description")
  422. }
  423. if tm.Name != nil {
  424. t.FriendlyName = optional.ToString(tm.Name)
  425. forceSend("FriendlyName")
  426. }
  427. if tm.Schema != nil {
  428. t.Schema = tm.Schema.toBQ()
  429. forceSend("Schema")
  430. }
  431. if tm.EncryptionConfig != nil {
  432. t.EncryptionConfiguration = tm.EncryptionConfig.toBQ()
  433. }
  434. if !validExpiration(tm.ExpirationTime) {
  435. return nil, fmt.Errorf("invalid expiration time: %v.\n"+
  436. "Valid expiration times are after 1678 and before 2262", tm.ExpirationTime)
  437. }
  438. if tm.ExpirationTime == NeverExpire {
  439. t.NullFields = append(t.NullFields, "ExpirationTime")
  440. } else if !tm.ExpirationTime.IsZero() {
  441. t.ExpirationTime = tm.ExpirationTime.UnixNano() / 1e6
  442. forceSend("ExpirationTime")
  443. }
  444. if tm.TimePartitioning != nil {
  445. t.TimePartitioning = tm.TimePartitioning.toBQ()
  446. t.TimePartitioning.ForceSendFields = []string{"RequirePartitionFilter"}
  447. if tm.TimePartitioning.Expiration == 0 {
  448. t.TimePartitioning.NullFields = []string{"ExpirationMs"}
  449. }
  450. }
  451. if tm.ViewQuery != nil {
  452. t.View = &bq.ViewDefinition{
  453. Query: optional.ToString(tm.ViewQuery),
  454. ForceSendFields: []string{"Query"},
  455. }
  456. }
  457. if tm.UseLegacySQL != nil {
  458. if t.View == nil {
  459. t.View = &bq.ViewDefinition{}
  460. }
  461. t.View.UseLegacySql = optional.ToBool(tm.UseLegacySQL)
  462. t.View.ForceSendFields = append(t.View.ForceSendFields, "UseLegacySql")
  463. }
  464. labels, forces, nulls := tm.update()
  465. t.Labels = labels
  466. t.ForceSendFields = append(t.ForceSendFields, forces...)
  467. t.NullFields = append(t.NullFields, nulls...)
  468. return t, nil
  469. }
  470. // validExpiration ensures a specified time is either the sentinel NeverExpire,
  471. // the zero value, or within the defined range of UnixNano. Internal
  472. // represetations of expiration times are based upon Time.UnixNano. Any time
  473. // before 1678 or after 2262 cannot be represented by an int64 and is therefore
  474. // undefined and invalid. See https://godoc.org/time#Time.UnixNano.
  475. func validExpiration(t time.Time) bool {
  476. return t == NeverExpire || t.IsZero() || time.Unix(0, t.UnixNano()).Equal(t)
  477. }
  478. // TableMetadataToUpdate is used when updating a table's metadata.
  479. // Only non-nil fields will be updated.
  480. type TableMetadataToUpdate struct {
  481. // The user-friendly description of this table.
  482. Description optional.String
  483. // The user-friendly name for this table.
  484. Name optional.String
  485. // The table's schema.
  486. // When updating a schema, you can add columns but not remove them.
  487. Schema Schema
  488. // The table's encryption configuration. When calling Update, ensure that
  489. // all mutable fields of EncryptionConfig are populated.
  490. EncryptionConfig *EncryptionConfig
  491. // The time when this table expires. To remove a table's expiration,
  492. // set ExpirationTime to NeverExpire. The zero value is ignored.
  493. ExpirationTime time.Time
  494. // The query to use for a view.
  495. ViewQuery optional.String
  496. // Use Legacy SQL for the view query.
  497. UseLegacySQL optional.Bool
  498. // TimePartitioning allows modification of certain aspects of partition
  499. // configuration such as partition expiration and whether partition
  500. // filtration is required at query time. When calling Update, ensure
  501. // that all mutable fields of TimePartitioning are populated.
  502. TimePartitioning *TimePartitioning
  503. labelUpdater
  504. }
  505. // labelUpdater contains common code for updating labels.
  506. type labelUpdater struct {
  507. setLabels map[string]string
  508. deleteLabels map[string]bool
  509. }
  510. // SetLabel causes a label to be added or modified on a call to Update.
  511. func (u *labelUpdater) SetLabel(name, value string) {
  512. if u.setLabels == nil {
  513. u.setLabels = map[string]string{}
  514. }
  515. u.setLabels[name] = value
  516. }
  517. // DeleteLabel causes a label to be deleted on a call to Update.
  518. func (u *labelUpdater) DeleteLabel(name string) {
  519. if u.deleteLabels == nil {
  520. u.deleteLabels = map[string]bool{}
  521. }
  522. u.deleteLabels[name] = true
  523. }
  524. func (u *labelUpdater) update() (labels map[string]string, forces, nulls []string) {
  525. if u.setLabels == nil && u.deleteLabels == nil {
  526. return nil, nil, nil
  527. }
  528. labels = map[string]string{}
  529. for k, v := range u.setLabels {
  530. labels[k] = v
  531. }
  532. if len(labels) == 0 && len(u.deleteLabels) > 0 {
  533. forces = []string{"Labels"}
  534. }
  535. for l := range u.deleteLabels {
  536. nulls = append(nulls, "Labels."+l)
  537. }
  538. return labels, forces, nulls
  539. }