Skip to content

EDM-683: Normalize Device Labels into a Separate Table for Optimized Queries and Fast Distinct Label Retrieval#845

Merged
asafbss merged 3 commits intomainfrom
EDM-683
Feb 10, 2025
Merged

EDM-683: Normalize Device Labels into a Separate Table for Optimized Queries and Fast Distinct Label Retrieval#845
asafbss merged 3 commits intomainfrom
EDM-683

Conversation

@asafbss
Copy link
Copy Markdown
Collaborator

@asafbss asafbss commented Jan 29, 2025

Summary

This PR introduces a structured approach to managing device labels by normalizing them into a separate device_labels table while retaining the existing JSONB-based storage in devices. The new structure significantly improves query performance, enables efficient lookups, and allows direct retrieval of distinct labels without iterating over all devices.

Key Changes

  • Introduce device_labels table to store labels as structured key-value pairs, optimizing queries.
  • Retain devices.labels JSONB field to ensure efficient device listing while enabling structured lookups.
  • Ensure device_labels references devices (org_id, name) as a foreign key with ON DELETE CASCADE.
  • Improve search performance by indexing label_key and label_value using pg_trgm for partial matching.
  • Add trigger-based synchronization to keep device_labels updated whenever devices.labels JSONB changes.
  • Enable fast retrieval of distinct labels using SQL queries instead of iterating over devices.

Optimized Querying of Distinct Labels

By leveraging the structured device_labels table, distinct label retrieval is now handled efficiently at the database level. Instead of iterating through JSONB fields, queries can directly fetch unique labels using indexed lookups:

Summary by CodeRabbit

  • New Features

    • Enhanced device management with integrated label support for efficient filtering and retrieval.
    • Introduced a dedicated device label format in the API for clearer label representation.
    • Improved filtering capabilities, including enhanced handling of UUID-based queries.
    • Added support for managing device labels through a new schema in the API.
  • Refactor

    • Revised data structures and query configurations to boost consistency and performance.
  • Documentation

    • Updated API specifications and embedded documentation to reflect the new label features.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 29, 2025

Walkthrough

This pull request enhances device and resource management by updating the database migration and query building logic. Changes include introducing a new device label model and migrating it alongside devices, creating a GIN index and triggers for synchronizing device labels, and expanding selector functionality. The updates extend to list query configuration, error handling, and UUID support across selectors, while also revising the API specification to include device label lists.

Changes

File(s) Change Summary
internal/store/device.go
internal/store/model/device.go
Enhanced DeviceStore to migrate a new DeviceLabel model, added GIN index for partial matches, trigger functions for synchronizing labels, and a new Labels method; introduced DeviceLabel type with foreign key constraints and UUID support.
internal/store/model/resource.go Updated GORM tags to change from a simple to a composite primary key and modified indexing for OrgID, Name, and Owner fields.
internal/store/common.go Introduced ListQueryOption and WithSelectorResolver to allow dynamic configuration of list queries; added the resolveOrDefault method and modified ListQuery to accept resolver options.
internal/store/model/selectors.go Added MapSelectorName and ListSelectors methods for the DeviceLabel type to improve selector resolution.
internal/store/selector/{annotation, field, label, resolvers, selector, types}
(including corresponding test files)
Updated selector parsing methods to replace the dest parameter with a resolver parameter, added UUID support, refined error handling, renamed and restructured methods (e.g. Parse, resolveSelector, List), and updated tests to use the new resolver initialization.
pkg/k8s/selector/selector.go Added new import for github.com/google/uuid and enhanced error handling in NewRequirement by validating UUID strings as acceptable inputs.
api/v1alpha1/{openapi.yaml, spec.gen.go, types.gen.go} Introduced a new DeviceLabelList schema in the OpenAPI spec, replaced Base64 encoded Swagger spec content in spec.gen.go, and added a new type definition for DeviceLabelList in the API types.

Sequence Diagram(s)

sequenceDiagram
  participant App as Application
  participant DB as Database
  participant Trigger as Sync_Trigger
  App->>DB: Insert/Update device (with labels JSONB)
  DB->>Trigger: Execute trigger function (sync_device_labels)
  Trigger->>DB: Update device_labels table based on labels change
  DB-->>App: Return acknowledgement
Loading
sequenceDiagram
  participant Client
  participant Store as DeviceStore
  participant Resolver as SelectorResolver
  participant DB as Database
  Client->>Store: Call Labels(ctx, orgId, listParams)
  Store->>Resolver: Resolve selectors via provided options
  Resolver-->>Store: Return resolved field mappings
  Store->>DB: Execute query using resolved selectors
  DB-->>Store: Return query results
  Store-->>Client: Return device labels
Loading

Possibly related PRs

Suggested reviewers

  • fzdarsky
  • avishayt
  • celdrake
  • hexfusion
  • ori-amizur
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 175ae04 and 8bfa651.

📒 Files selected for processing (3)
  • internal/store/device.go (2 hunks)
  • internal/store/model/device.go (1 hunks)
  • internal/store/model/resource.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: e2e
  • GitHub Check: smoke
  • GitHub Check: integration-tests
🔇 Additional comments (3)
internal/store/device.go (3)

76-76: Include DeviceLabel in auto-migration.
Auto-migrating DeviceLabel alongside Device ensures the new table is created in sync with the existing schema. This looks good.


154-167: Validate partial matching index usage.
Creating GIN indexes with pg_trgm for label_key and label_value is a good approach for partial matching. Ensure your queries (e.g., ILIKE) actually leverage these indexes.


168-211: Assess performance of JSONB-to-table trigger approach.
The trigger logic cleanly synchronizes device_labels with the labels JSONB field. However, large or frequent JSON updates could impact performance. Consider monitoring or batching updates if the label sets grow significantly.

Comment thread internal/store/model/resource.go Outdated
Comment thread internal/store/model/device.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/store/model/device.go (1)

43-51: LGTM! Consider adding a composite index for label searches.

The DeviceLabel struct is well-designed with appropriate primary keys and indexes. The foreign key constraint with CASCADE DELETE ensures data consistency.

Consider adding a composite index on (org_id, label_key, label_value) to optimize queries that search for devices by label key-value pairs within an organization:

 type DeviceLabel struct {
 	OrgID      uuid.UUID `gorm:"primaryKey;type:uuid;index:,composite:device_label_org_device"`
 	DeviceName string    `gorm:"primaryKey;index:,composite:device_label_org_device"`
 	LabelKey   string    `gorm:"primaryKey;index:,composite:device_label_key"`
-	LabelValue string    `gorm:"index"`
+	LabelValue string    `gorm:"index;index:,composite:device_label_search"`
 
 	// Foreign Key Constraint with CASCADE DELETE
 	Device Device `gorm:"foreignKey:OrgID,DeviceName;references:OrgID,Name;constraint:OnDelete:CASCADE"`
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bfa651 and 4e7f0c2.

📒 Files selected for processing (3)
  • internal/store/device.go (2 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/resource.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/store/model/resource.go
⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: unit-tests
  • GitHub Check: smoke
  • GitHub Check: integration-tests
  • GitHub Check: lint
  • GitHub Check: e2e
🔇 Additional comments (3)
internal/store/device.go (3)

76-76: LGTM! Migration includes both models.

The migration correctly includes both Device and DeviceLabel models.


154-166: LGTM! Efficient indexing for partial matching.

The implementation correctly:

  1. Enables the pg_trgm extension for partial matching support
  2. Creates a GIN index using gin_trgm_ops for both label_key and label_value

168-211: LGTM! Robust trigger-based synchronization.

The trigger function implementation is thorough and efficient:

  1. Handles both INSERT and UPDATE operations
  2. Properly cleans up deleted labels during updates
  3. Uses UPSERT for atomic updates
  4. Includes condition to avoid unnecessary trigger execution

Let's verify the trigger behavior:

✅ Verification successful

Trigger implementation verified as robust and safe

The implementation is well-designed with multiple safety layers:

  • Input validation ensures K8s-compliant labels
  • PostgreSQL's jsonb_each_text safely handles and flattens any complex JSONB structures
  • Proper cleanup and atomic updates maintain data consistency
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify trigger function behavior for edge cases

# Test: Check if trigger handles JSON arrays or nested objects
rg -A 5 'labels.*jsonb.*array|labels.*jsonb.*object' internal/store/

# Test: Check if there are any existing label synchronization functions
rg -A 5 'sync.*label|label.*sync' internal/store/

Length of output: 1646


Script:

#!/bin/bash
# Check devices table schema and constraints
ast-grep --pattern 'type Device struct {
  $$$
  Labels $_ `$$$`
  $$$
}'

# Look for JSONB validation
rg -B2 -A2 'validate.*labels|labels.*validate' internal/

Length of output: 943

@asafbss asafbss changed the title EDM-683: normalize device labels into separate table while retaining JSONB for fast listing EDM-683: Normalize Device Labels into a Separate Table for Optimized Queries and Fast Distinct Label Retrieval Jan 29, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
internal/store/common.go (2)

71-87: Consider handling the resolver creation error explicitly.
When selector.SelectorFieldResolver returns an error, q.resolver silently defaults to selector.EmptyResolver{}. This might mask underlying schema issues.

You could log or track the error to help diagnose unexpected schema resolution failures.

 if q.resolver == nil {
   resolver, err := selector.SelectorFieldResolver(q.dest)
   if err != nil {
-    q.resolver = selector.EmptyResolver{}
+    log.Errorf("failed to create resolver for %T: %v", q.dest, err)
+    q.resolver = selector.EmptyResolver{}
   } else {
     q.resolver = resolver
   }
 }

134-144: Optional logging for resolution failure.
When ResolveFields returns an error, you return the default. This is fine if the default is the intended fallback; otherwise, you may consider logging or distinguishing empty resolution vs. error.

pkg/k8s/selector/selector.go (1)

232-234: LGTM! UUID validation is properly implemented.

The addition of UUID validation as a fallback is well-placed, maintaining the existing validation flow while adding support for UUID fields. The error message could be more specific about UUID support.

Consider updating the error message to explicitly mention UUID support:

-							allErrs = append(allErrs, field.Invalid(valuePath.Index(i), vals[i][j], "for 'Gt', 'Lt', 'Gte', and 'Lte' operators, the value must be a number or a valid time in RFC3339 format"))
+							allErrs = append(allErrs, field.Invalid(valuePath.Index(i), vals[i][j], "for 'Gt', 'Lt', 'Gte', and 'Lte' operators, the value must be a number, a valid time in RFC3339 format, or a valid UUID"))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7f0c2 and ac42929.

📒 Files selected for processing (15)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/store/model/device.go
⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: smoke
  • GitHub Check: e2e
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: integration-tests
🔇 Additional comments (61)
internal/store/common.go (6)

58-65: Use of functional options is clear and flexible.
This pattern makes it easier to customize the query behavior and injector logic. The approach is well-structured.


67-68: New field “resolver” in listQuery struct.
Adding the resolver field aligns with the new functional options approach. Just ensure any concurrent usage of listQuery is carefully managed since it holds mutable state.


93-95: Confirm that “metadata.orgid” is consistently spelled.
Make sure you standardize the naming, e.g., “metadata.orgid” vs. “metadata.orgId,” to avoid confusion in the codebase.

Would you like to run a codebase search to confirm consistent usage of "metadata.orgid"?


98-99: Field selector resolution is cleanly integrated.
Invoking Parse with the new resolver is consistent with the updated design. No issues noted.


106-107: Label selector usage aligns with the new parsing flow.
This is consistent with the approach for handling hidden selector names. Looks good.


115-116: Annotation selector resolution mirrors label selector pattern.
It’s symmetrical with the label selector logic. No immediate concerns.

internal/store/selector/field.go (19)

19-19: Importing uuid is appropriate for new UUID logic.
No concerns here.


29-33: New selectorParserSession struct consolidates selector + resolver.
This design unifies the contextual parsing data, making the code more explicit and testable.


167-169: Updated documentation clarifies usage of resolver.
This helps maintain clarity on how the parse method works with the new resolver.


173-174: Parameter doc lines are helpful.
Good to see thorough documentation regarding ctx and resolver.


180-180: Return signature adds clarity on error conditions.
Looks good.


192-195: Check resolver non-nil requirement.
This early check prevents downstream nil reference errors.


197-197: Parser initialization updated to respect new flow.
Approach appears solid.


206-206: Use of selectorParserSession.
Makes sense to pass both the selector & resolver together for parsing.


208-208: Error type check ensures correct handling of known selector errors.
No issues noted.


223-226: Ensuring input is a selectorParserSession.
This safety check helps avoid unexpected type assertion panics.


228-230: Resolver presence check.
This guards against nil usage if something is misconfigured.


232-232: Retrieving requirements from session.selector.
Straightforward extraction.


243-243: Custom logic for JSONB array casting.
Correctly flags unsupported cases.


249-253: Improved error message for unknown selector name.
Clarifying that the user can see supported selectors is very helpful.


301-301: Wrap multiple requirements in an AND token.
This is consistent with multi-clause handling.


422-422: Refactoring if private field is used.
The error message is clear that private fields can’t be accessed.


429-435: UUID parsing introduced in resolveValue.
Error handling is correct, returning parse failure immediately.


510-511: Checking for “UUID” type in resolveQuery.
Extends the parser for new type coverage.


529-541: applyUUIDOperator design covers standard operators.
Supports equality, inequality, range comparisons, and membership checks. Good approach.

internal/store/selector/resolvers.go (6)

1-27: EmptyResolver for no-op handling.
These methods returning empty slices ensure safe fallback without unexpected panics.


28-31: CompositeSelectorResolver struct design.
Holds a map of table-based resolvers, facilitating multi-model usage. Looks flexible.


33-56: NewCompositeSelectorResolver processes multiple models.
Fetching GORM schema for each, then building a field resolver is a neat approach. Check potential concurrency if multiple goroutines might share resolvers.


58-73: ResolveNames aggregates across all resolvers.
Prefixed table name is correct to avoid collisions in multi-model queries.


75-91: ResolveFields merges field metadata with table name prefix.
Gracefully short-circuits if it finds a matching set in the first resolver.


93-107: List gathers all known selector names.
Sorting them is helpful for stable output. Good use of a set to avoid duplicates.

internal/store/selector/annotation_test.go (2)

25-29: LGTM! Proper error handling for resolver initialization.

The initialization of the resolver with appropriate error handling is well implemented.


38-38: LGTM! Updated Parse call to use resolver.

The Parse method call has been correctly updated to use the new resolver parameter.

internal/store/selector/selector_test.go (3)

28-28: LGTM! Added UUID field with proper type and selector tag.

The UUID field is correctly added with appropriate GORM and selector tags.


92-92: LGTM! Added GoodSelector field with proper selector tag.

The GoodSelector field is correctly added with the appropriate selector tag.


174-176: LGTM! Updated unknown selector test logic.

The test logic for unknown selectors has been updated to check the length of resolved fields instead of expecting an error, which is more precise.

internal/store/model/selectors.go (2)

71-79: LGTM! Well-implemented MapSelectorName for DeviceLabel.

The implementation correctly maps the metadata.label.keyorvalue selector to both key and value fields, following the established pattern in the codebase.


81-83: LGTM! Consistent implementation of ListSelectors for DeviceLabel.

The ListSelectors method follows the same pattern as other types and correctly exposes the metadata.label.keyorvalue selector.

internal/store/selector/types.go (3)

14-14: LGTM! Added UUID selector type with proper schema resolution.

The UUID selector type is correctly added and properly mapped in the schemaTypeResolution map.

Also applies to: 33-33


67-79: LGTM! Well-documented Resolver interface.

The Resolver interface is clearly documented with comprehensive method descriptions that explain the purpose and functionality of each method.


175-176: LGTM! Added UUID case to String method.

The String method has been properly updated to handle the new UUID type.

internal/store/selector/label_test.go (2)

33-37: LGTM! Good error handling for resolver initialization.

The error handling for the resolver initialization is well-implemented, with clear error messages and early return on failure.


46-46: LGTM! Updated Parse call to use the resolver.

The Parse method call has been correctly updated to use the resolver, aligning with the changes in the selector interface.

internal/store/selector/label.go (4)

100-118: LGTM! Well-documented method signature.

The updated documentation clearly explains the method's parameters, return values, and provides a good example usage.


122-126: LGTM! Good defensive programming with resolver validation.

The nil check for the resolver is a good practice that prevents potential runtime panics.


155-156: LGTM! Clear field resolution logic.

The comment and implementation for resolving selector fields using the provided resolver is clear and maintainable.


182-185: LGTM! Improved error handling.

The error handling has been enhanced to properly handle selector-specific errors, improving error reporting.

internal/store/selector/annotation.go (3)

127-134: LGTM! Consistent documentation style.

The documentation follows the same style as label.go, maintaining consistency across the codebase.


150-153: LGTM! Consistent resolver validation.

The resolver validation matches the implementation in label.go, ensuring consistent behavior.


182-186: LGTM! Consistent error handling.

The error handling pattern matches label.go, maintaining a consistent approach across selectors.

internal/store/selector/selector.go (3)

31-31: LGTM! Interface-based return type.

Returning the Resolver interface instead of the concrete type improves abstraction and flexibility.


201-203: LGTM! Added field length validation.

The validation ensures consistency between the number of fields and selector names, preventing partial resolutions.


227-230: LGTM! Efficient hidden selector filtering.

Using slices.DeleteFunc for filtering hidden selectors is an efficient and idiomatic approach.

internal/store/selector/field_test.go (4)

33-33: LGTM! Good test coverage for valid UUID.

The test case properly validates UUID string format support.


46-46: LGTM! Invalid UUID test case is included.

The test case ensures proper error handling for invalid UUID formats.


101-107: LGTM! Comprehensive test coverage for UUID operations.

The test cases cover all essential UUID operations including existence checks, equality comparisons, and set operations.


200-201: LGTM! Invalid operations are properly tested.

The test cases correctly identify unsupported operations for UUID fields.

internal/store/device.go (4)

78-78: LGTM! Device label model is included in migration.

The migration properly includes both Device and DeviceLabel models.


156-168: LGTM! Efficient indexing for partial matching.

The implementation correctly:

  • Ensures pg_trgm extension is available
  • Creates GIN index for both key and value fields
  • Only applies to PostgreSQL dialect

237-267: LGTM! Well-implemented label retrieval method.

The Labels method implementation:

  • Uses composite selector resolver
  • Properly joins device_labels table
  • Handles pagination through limit parameter
  • Returns a map of key-value pairs

Let's verify the query performance:


170-213: LGTM! Robust trigger implementation for label synchronization.

The trigger implementation:

  • Handles both INSERT and UPDATE operations
  • Properly manages label deletions
  • Uses DISTINCT FROM for efficient change detection
  • Implements UPSERT pattern for label updates

Let's verify the trigger behavior:

✅ Verification successful

Verified trigger implementation in internal/store/device.go

The search confirms that the trigger function sync_device_labels along with both device_labels_insert and device_labels_update triggers are correctly defined in the codebase.

  • Trigger function is defined at line 176.
  • Insert trigger is defined at line 199.
  • Update trigger is defined at line 204.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify trigger function and its usage
# Test: Check if the trigger function exists and is properly attached
psql -c "\df sync_device_labels" && \
psql -c "\d+ devices" | grep -A 2 "Triggers:"

Length of output: 129


Script:

#!/bin/bash
# Description: Re-verify that the trigger function and its attached triggers are defined in the codebase.
# We search for the trigger function and the triggers in the internal/store/device.go file.

rg -n "CREATE OR REPLACE FUNCTION sync_device_labels" internal/store/device.go && \
rg -n "CREATE TRIGGER device_labels_insert" internal/store/device.go && \
rg -n "CREATE TRIGGER device_labels_update" internal/store/device.go

Length of output: 360

@asafbss asafbss force-pushed the EDM-683 branch 2 times, most recently from 874b38f to 9c0706b Compare February 3, 2025 20:46
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
pkg/k8s/selector/selector.go (1)

232-234: Update error message to include UUID format.

The error message should be updated to mention UUID as a valid format since it's now supported.

Apply this diff to improve the error message:

-							allErrs = append(allErrs, field.Invalid(valuePath.Index(i), vals[i][j], "for 'Gt', 'Lt', 'Gte', and 'Lte' operators, the value must be a number or a valid time in RFC3339 format"))
+							allErrs = append(allErrs, field.Invalid(valuePath.Index(i), vals[i][j], "for 'Gt', 'Lt', 'Gte', and 'Lte' operators, the value must be a number, a valid time in RFC3339 format, or a valid UUID"))
internal/store/common.go (1)

93-95: Consider using a constant for the metadata.orgid selector name.

The hardcoded selector name could be moved to a constant to improve maintainability and reusability.

+const metadataOrgIDSelector = "metadata.orgid"

-fmt.Sprintf("%s = ?", lq.resolveOrDefault(
-  selector.NewHiddenSelectorName("metadata.orgid"), "org_id")), orgId)
+fmt.Sprintf("%s = ?", lq.resolveOrDefault(
+  selector.NewHiddenSelectorName(metadataOrgIDSelector), "org_id")), orgId)
internal/store/selector/annotation.go (1)

182-186: Consider extracting error handling logic.

The error handling pattern is repeated in multiple places. Consider extracting it to a helper function.

+func wrapSelectorError(err error) error {
+    if IsSelectorError(err) {
+        return err
+    }
+    return NewSelectorError(flterrors.ErrAnnotationSelectorParseFailed, err)
+}

-if IsSelectorError(err) {
-    return "", nil, err
-}
-return "", nil, NewSelectorError(flterrors.ErrAnnotationSelectorParseFailed, err)
+return "", nil, wrapSelectorError(err)
internal/store/selector/selector.go (1)

108-110: Consider adding a comment explaining the reset logic.

The logic to reset fields when lengths don't match would benefit from a comment explaining the rationale.

+// Reset fields if not all selectors were resolved to maintain consistency
 if len(fields) > 0 && len(fields) != len(selectorNames) {
     fields = make([]*SelectorField, 0)
 }
internal/store/selector/field_test.go (1)

49-53: Consider extracting test setup to a helper function.

The resolver setup code is repeated in multiple test functions. Consider extracting it to a helper function.

+func setupTestResolver(t *testing.T) Resolver {
+    resolver, err := SelectorFieldResolver(&goodTestModel{})
+    if err != nil {
+        t.Fatalf("error setting up test resolver: %v", err)
+    }
+    return resolver
+}

-resolver, err := SelectorFieldResolver(&goodTestModel{})
-if err != nil {
-    t.Errorf("error %v (%#v)\n", err, err)
-    return
-}
+resolver := setupTestResolver(t)

Also applies to: 264-269

internal/store/device.go (2)

170-213: Consider adding error handling for trigger deletion.

While the trigger implementation is solid, consider handling potential errors when dropping existing triggers.

Apply this diff to improve error handling:

-		DROP TRIGGER IF EXISTS device_labels_insert ON devices;
-		DROP TRIGGER IF EXISTS device_labels_update ON devices;
+		DO $$
+		BEGIN
+			DROP TRIGGER IF EXISTS device_labels_insert ON devices;
+			DROP TRIGGER IF EXISTS device_labels_update ON devices;
+		EXCEPTION WHEN OTHERS THEN
+			-- Log error and continue, as the trigger might not exist
+			RAISE NOTICE 'Error dropping triggers: %', SQLERRM;
+		END $$;

237-267: Consider adding pagination support.

The Labels method implementation looks good, but consider adding pagination support for better performance with large datasets.

Apply this diff to add pagination:

 func (s *DeviceStore) Labels(ctx context.Context, orgId uuid.UUID, listParams ListParams) (map[string]string, error) {
 	var labels []model.DeviceLabel
+	var offset int
+	if listParams.Page > 0 {
+		offset = (listParams.Page - 1) * listParams.Limit
+	}
 
 	resolver, err := selector.NewCompositeSelectorResolver(&model.Device{}, &model.DeviceLabel{})
 	if err != nil {
 		return nil, fmt.Errorf("failed to create selector resolver: %w", err)
 	}
 
 	query, err := ListQuery(model.Device{}, WithSelectorResolver(resolver)).BuildNoOrder(ctx, s.db, orgId, listParams)
 	if err != nil {
 		return nil, err
 	}
 
 	query = query.Select("DISTINCT device_labels.label_key, device_labels.label_value").
 		Joins("JOIN device_labels ON devices.org_id = device_labels.org_id AND devices.name = device_labels.device_name")
 
 	if listParams.Limit > 0 {
 		query = query.Limit(listParams.Limit)
+		if offset > 0 {
+			query = query.Offset(offset)
+		}
 	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac42929 and 9c0706b.

📒 Files selected for processing (16)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/resource.go (1 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/store/selector/label_test.go
  • internal/store/model/device.go
  • internal/store/selector/label.go
  • internal/store/selector/resolvers.go
  • internal/store/selector/types.go
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: smoke
  • GitHub Check: e2e
🔇 Additional comments (23)
pkg/k8s/selector/selector.go (1)

33-33: LGTM!

The addition of the github.com/google/uuid import is correctly placed and necessary for UUID parsing support.

internal/store/model/resource.go (2)

12-13: LGTM! Clear documentation of the composite primary key.

The comment clearly documents the composite primary key structure, which is essential for understanding the model's uniqueness constraints.


16-16: LGTM! Well-structured composite key and index definitions.

The GORM tags correctly define:

  1. A composite primary key using OrgID and Name
  2. A composite index org_name with proper field priorities
  3. A simple index owner_idx for the Owner field

This structure aligns well with the PR's goal of optimizing queries while maintaining proper multi-tenant data isolation.

Also applies to: 21-21, 27-27

internal/store/selector/annotation_test.go (2)

25-29: LGTM! Proper error handling for resolver initialization.

The code correctly initializes the resolver and handles potential errors, preventing null pointer dereferences in subsequent operations.


38-38: LGTM! Updated Parse method call with resolver.

The code correctly uses the resolver pattern for parsing annotations, which aligns with the PR's goal of improving query capabilities.

internal/store/selector/selector_test.go (3)

28-28: LGTM! Added UUID field with proper GORM and selector tags.

The Field18 definition correctly includes both GORM and selector tags, enhancing test coverage for UUID handling.


92-93: LGTM! Added GoodSelector field with proper selector tag.

The GoodSelector field is correctly defined with a selector tag, improving test coverage for selector resolution.


156-171: LGTM! Enhanced field resolution validation.

The code now properly validates both the presence of resolved fields and names, improving error detection and reporting.

internal/store/model/selectors.go (2)

72-80: LGTM! Well-implemented MapSelectorName method for DeviceLabel.

The method correctly maps the composite selector to individual key and value selectors, enabling flexible label querying.


82-84: LGTM! Consistent ListSelectors implementation for DeviceLabel.

The method follows the established pattern for selector registration, maintaining consistency across the codebase.

internal/store/common.go (3)

58-64: LGTM! Well-designed functional options pattern.

The introduction of ListQueryOption and WithSelectorResolver follows the functional options pattern, providing a clean and extensible way to configure listQuery instances.


71-88: LGTM! Good error handling and default resolver setup.

The ListQuery function properly handles options and provides a sensible default resolver when none is provided. The error handling for resolver creation is appropriate.


134-143: LGTM! Robust field resolution with fallback.

The resolveOrDefault method provides a clean way to resolve fields with a fallback value, properly handling errors and nil cases.

internal/store/selector/annotation.go (2)

127-148: LGTM! Well-documented API changes.

The documentation updates are thorough and include clear examples of the new resolver-based API.


149-153: LGTM! Good error handling for resolver validation.

The early validation of the resolver parameter prevents null pointer issues later in the code.

internal/store/selector/selector.go (2)

31-45: LGTM! Good interface-based design.

The change to return a Resolver interface instead of a concrete type improves abstraction and flexibility.


227-231: LGTM! Good use of slices.DeleteFunc for filtering.

The use of slices.DeleteFunc for filtering hidden selectors is clean and efficient.

internal/store/selector/field_test.go (1)

33-34: LGTM! Good test coverage for UUID handling.

The addition of UUID test cases improves the test coverage for both valid and invalid scenarios.

Also applies to: 46-47

internal/store/selector/field.go (3)

29-32: LGTM! Good encapsulation of selector and resolver.

The selectorParserSession struct provides a clean way to bundle the selector and resolver together, improving code organization.


191-195: Good defensive programming with resolver validation.

The early validation of the resolver parameter prevents potential nil pointer dereferences later in the code.


429-435: LGTM! Robust UUID parsing and validation.

The implementation properly handles UUID parsing and validation using the google/uuid package.

internal/store/device.go (2)

78-78: LGTM! Proper model migration order.

The migration correctly includes both Device and DeviceLabel models in the correct order.


156-168: Good use of GIN index for partial matching.

The implementation correctly sets up GIN indexes with trigram operations for efficient partial matching on label keys and values.

Comment thread internal/store/selector/field.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🔭 Outside diff range comments (1)
api/v1alpha1/openapi.yaml (1)

16-30: Warning: Undefined Operation Tag device-labels in Global Tags

The new /api/v1/devicelabels endpoint uses the tag device-labels, but this tag is not defined in the global tags section. To satisfy the API guidelines and suppress the pipeline warning, please add the device-labels tag to the global tags list. For example, adjust the tags section as follows:

 tags:
   - name: authentication
     description: Operations for authentication.
   - name: certificatesigningrequest
     description: Operations on CertificateSigningRequest resources.
   - name: device
     description: Operations on Device resources.
   - name: enrollmentrequest
     description: Operations on EnrollmentRequest resources.
   - name: fleet
     description: Operations on Fleet resources.
   - name: repository
     description: Operations on Repository resources.
   - name: resourcesync
     description: Operations on ResourceSync resources.
+  - name: device-labels
+    description: Operations on device label mappings.

This ensures consistency across your API specification by defining all operation tags globally.

🧹 Nitpick comments (2)
internal/service/device.go (1)

150-204: Consider adding pagination or continue param

While the method provides a limit option, it does not parse or handle a continue token (like in ListDevices), which may become problematic for large label sets. Having a consistent approach to pagination across listing methods helps maintain uniformity and efficiency.

internal/store/device.go (1)

156-169: Assess partial matching indexes

Creating a single GIN index on both label_key and label_value with pg_trgm is a strong step for substring searches combining both fields. If your queries often involve filtering on just one column, consider separate indexes for potential performance gains.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c0706b and fb3715e.

📒 Files selected for processing (7)
  • api/v1alpha1/openapi.yaml (2 hunks)
  • api/v1alpha1/spec.gen.go (1 hunks)
  • api/v1alpha1/types.gen.go (2 hunks)
  • internal/api/client/client.gen.go (7 hunks)
  • internal/api/server/server.gen.go (7 hunks)
  • internal/service/device.go (1 hunks)
  • internal/store/device.go (5 hunks)
🧰 Additional context used
🪛 GitHub Actions: API Quality
api/v1alpha1/openapi.yaml

[error] 925-925: Verb /api/v1/devicelabels.post is required.


[error] 925-925: Verb /api/v1/devicelabels.delete is required.


[warning] 928-928: Operation tags must be defined in global tags.

⏰ Context from checks skipped due to timeout of 90000ms (12)
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: smoke
  • GitHub Check: integration-tests
  • GitHub Check: lint
  • GitHub Check: e2e
🔇 Additional comments (14)
api/v1alpha1/spec.gen.go (1)

21-243: Validate the newly embedded Swagger specification

You're replacing the entire Base64-encoded content with new data. Please verify that the embedded Swagger specification aligns with your new endpoints, especially /api/v1/devicelabels. Consider running an OpenAPI validator or integration test to confirm that the generated documentation and stubs are valid and up to date.

internal/store/device.go (4)

13-13: New import usage looks fine

The import github.com/flightctl/flightctl/internal/store/selector matches your usage below; no issues found.


29-29: Labeled interface addition

Adding Labels(...) to the interface provides a convenient way to fetch label data. The signature is consistent with the existing interface patterns.


170-214: Trigger-based device_labels synchronization

The trigger function and new triggers keep the device_labels table in sync with changes to the devices.labels JSONB. This approach looks correct for both insert and update operations. If concurrent updates on the same row are likely, ensure transactional consistency is acceptable.


237-271: Verify potential label value overwriting

When multiple devices share the same label key with different values, this logic overwrites previous values in your final labelsMap. If that is unintentional, consider returning an array of values per key to avoid data loss.

Below is a possible approach if you need to accumulate multiple values for each key:

- func (s *DeviceStore) Labels(ctx context.Context, orgId uuid.UUID, listParams ListParams) (map[string]string, error) {
+ func (s *DeviceStore) Labels(ctx context.Context, orgId uuid.UUID, listParams ListParams) (map[string][]string, error) {

   // ...
   labelsMap := make(map[string][]string, len(labels))
   for _, label := range labels {
-     labelsMap[label.LabelKey] = label.LabelValue
+     labelsMap[label.LabelKey] = append(labelsMap[label.LabelKey], label.LabelValue)
   }
   return labelsMap, nil
}
api/v1alpha1/types.gen.go (2)

418-419: LGTM! Well-defined type for device labels.

The DeviceLabelMap type is appropriately defined as a map with string keys and values, which aligns well with the PR's objective of normalizing device labels.


1454-1464: LGTM! Parameters struct follows established patterns.

The ListDeviceLabelsParams struct is well-structured with:

  • Standard filtering capabilities via label and field selectors
  • Pagination support through the limit parameter
  • Clear and consistent documentation
internal/api/server/server.gen.go (3)

1-4: Note: This is an auto-generated file.

This file is generated by oapi-codegen v2.3.0 and should not be manually modified. Any changes should be made to the OpenAPI specification and the file should be regenerated.


51-52: LGTM: New endpoint implementation is consistent.

The new ListDeviceLabels endpoint is properly integrated with:

  • Interface definition
  • Default implementation
  • Request/Response types
  • Middleware handler

The implementation follows the same patterns as other endpoints in the file.

Also applies to: 268-271, 3213-3264, 6914-6938


2493-2495: LGTM: Router configuration is properly updated.

The new endpoint is correctly registered in the router configuration with the appropriate path and handler.

internal/api/client/client.gen.go (4)

131-132: LGTM! Interface method addition is well-defined.

The new ListDeviceLabels method is properly added to the ClientInterface with appropriate parameters and return types.


1874-1953: LGTM! Request generator implementation follows established patterns.

The NewListDeviceLabelsRequest function correctly:

  • Handles server URL parsing
  • Constructs the API endpoint path
  • Processes query parameters (labelSelector, fieldSelector, limit)
  • Creates and returns the HTTP request

5025-5049: LGTM! Response type definition is complete.

The ListDeviceLabelsResponse struct properly includes:

  • Raw response body
  • HTTP response object
  • Typed response fields for success (200) and error cases (400, 401, 403, 503)

7788-7840: LGTM! Response parser implementation is thorough.

The ParseListDeviceLabelsResponse function correctly:

  • Reads and closes the response body
  • Handles content type checking
  • Unmarshals JSON responses into appropriate types
  • Handles all defined response status codes

Comment thread api/v1alpha1/openapi.yaml Outdated
@asafbss
Copy link
Copy Markdown
Collaborator Author

asafbss commented Feb 4, 2025

Hey @celdrake ,

I want to test the solution to confirm that it meets the requirements.
Once validated, I'll initiate sessions to finalize the general design and API approval.

For testing, I’ve created a temporary endpoint (api/v1/devicelabels). While its final design is still TBD, it will allow you to verify the functionality. Please note that this will not be implemented via CLI until we reach a consensus. Also, only new devices will reflect the expected behavior for this requirement.

For listing distinct labels only (not for listing devices), three new selectors are introduced:

  • metadata.label.key
  • metadata.label.value
  • metadata.label.keyorvalue

My suggestion:
If the end-user uses an equals sign (=), apply metadata.label.key and metadata.label.value based on the left and right sides of the expression.
Otherwise, use metadata.label.keyorvalue.

Example query:

/api/v1/devicelabels?fieldSelector=metadata.label.keyorvalue%20contains%20bla&limit=10
Let me know once you've confirmed.

@asafbss asafbss requested a review from celdrake February 4, 2025 07:52
@celdrake
Copy link
Copy Markdown
Contributor

celdrake commented Feb 4, 2025

Hey @asafbss I don't think I'll have time for testing this PR in a few days, as I'm currently working in finishing the Decommissioning and Rollout policies in the UI.

@asafbss
Copy link
Copy Markdown
Collaborator Author

asafbss commented Feb 4, 2025

Hey @asafbss I don't think I'll have time for testing this PR in a few days, as I'm currently working in finishing the Decommissioning and Rollout policies in the UI.

Hey @celdrake , I completely understand, but I can't proceed with the design review without a basic acknowledgment that the solution meets the requirement. Keep in mind that the TP closes on February 12, including QE, and without approval of the entire solution lifecycle, it may not make it in.

Adding the solution after the TP could require a migration for the device labels data, which would add extreme complexity.

🙏 Please share your thoughts—I just need confirmation that it meets the requirement.

@celdrake
Copy link
Copy Markdown
Contributor

celdrake commented Feb 7, 2025

For testing, I’ve created a temporary endpoint (api/v1/devicelabels). While its final design is still TBD, it will allow you to verify the functionality. Please note that this will not be implemented via CLI until we reach a consensus. Also, only new
Example query:

/api/v1/devicelabels?fieldSelector=metadata.label.keyorvalue%20contains%20bla&limit=10
Let me know once you've confirmed.

Hi, @asafbss
I've tested the new endpoint, and I see a problem in its response type. As it returns a Map, it cannot return the list of all labels which match the query if they share the exact key. This is an issue as any user that only remembers the key won't be able to find the labels they are looking for.

Eg. The following requests does not return all labels that include "test" because there are multiple labels with a key equal to "test". (This happens either with metadata.label.key=test AND metadata.label.keyorvalue=test)

curl -k -X 'GET' 'https://api.192.168.1.132.nip.io:3443/api/v1/devicelabels?fieldSelector=metadata.label.key=test&limit=10' -H 'accept: application/json' -H "Authorization: Bearer $TOKEN"
{"test":"yes"}

I'd expect the response to be a List of unique labels matching the query (either as key=value strings or as `{key: 'just-a-test', value: 'yest'} objects

["just-a-test=yes", "testing=yes", "testing=no", "this-is=a-test", "test"]
devices-labels-endpoint-test.mp4

Comment thread api/v1alpha1/openapi.yaml Outdated
additionalProperties:
type: string
description: |
A map of distinct labels, where the keys are label names and the values are label values.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A map of distinct labels, where the keys are label names and the values are label values.
A map of distinct labels, where the keys are label keys and the values are label values.

which is quite redundant

As this likely will change, let's see what the best description for the new response.

@asafbss
Copy link
Copy Markdown
Collaborator Author

asafbss commented Feb 7, 2025

For testing, I’ve created a temporary endpoint (api/v1/devicelabels). While its final design is still TBD, it will allow you to verify the functionality. Please note that this will not be implemented via CLI until we reach a consensus. Also, only new
Example query:

/api/v1/devicelabels?fieldSelector=metadata.label.keyorvalue%20contains%20bla&limit=10
Let me know once you've confirmed.

Hi, @asafbss I've tested the new endpoint, and I see a problem in its response type. As it returns a Map, it cannot return the list of all labels which match the query if they share the exact key. This is an issue as any user that only remembers the key won't be able to find the labels they are looking for.

Eg. The following requests does not return all labels that include "test" because there are multiple labels with a key equal to "test". (This happens either with metadata.label.key=test AND metadata.label.keyorvalue=test)

curl -k -X 'GET' 'https://api.192.168.1.132.nip.io:3443/api/v1/devicelabels?fieldSelector=metadata.label.key=test&limit=10' -H 'accept: application/json' -H "Authorization: Bearer $TOKEN"
{"test":"yes"}

I'd expect the response to be a List of unique labels matching the query (either as key=value strings or as `{key: 'just-a-test', value: 'yest'} objects

["just-a-test=yes", "testing=yes", "testing=no", "this-is=a-test", "test"]

devices-labels-endpoint-test.mp4

Good catch, I'll return an Array instead of Map.
Thanks 🙏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
internal/store/selector/resolvers.go (1)

75-91: Consider aggregating fields from all resolvers.

The ResolveFields method returns early when fields are found in one resolver, potentially missing fields from other resolvers. Consider aggregating fields from all resolvers before returning.

Apply this diff to aggregate fields from all resolvers:

 func (r *CompositeSelectorResolver) ResolveFields(name SelectorName) ([]*SelectorField, error) {
+    var allFields []*SelectorField
     for table, resolver := range r.resolvers {
         fields, err := resolver.ResolveFields(name)
         if err != nil {
             return nil, err
         }
         if len(fields) > 0 {
             for _, field := range fields {
                 field.FieldName = fmt.Sprintf("%s.%s", table, field.FieldName)
             }
-            return fields, nil
+            allFields = append(allFields, fields...)
         }
     }
-    return []*SelectorField{}, nil
+    return allFields, nil
 }
internal/store/model/selectors.go (1)

82-84: Consider adding individual selectors to the list.

The method could also include "metadata.label.key" and "metadata.label.value" selectors for direct access.

Apply this diff to include individual selectors:

 func (m *DeviceLabel) ListSelectors() selector.SelectorNameSet {
-    return selector.NewSelectorFieldNameSet().Add(selector.NewSelectorName("metadata.label.keyorvalue"))
+    return selector.NewSelectorFieldNameSet().Add(
+        selector.NewSelectorName("metadata.label.keyorvalue"),
+        selector.NewSelectorName("metadata.label.key"),
+        selector.NewSelectorName("metadata.label.value"),
+    )
 }
internal/store/common.go (1)

71-88: Consider improving error handling for resolver initialization.

The error from SelectorFieldResolver is silently handled by falling back to EmptyResolver. Consider propagating the error or logging it.

Apply this diff to improve error handling:

 func ListQuery(dest any, opts ...ListQueryOption) *listQuery {
     q := &listQuery{dest: dest}

     for _, opt := range opts {
         opt(q)
     }

     // Set resolver if not provided
     if q.resolver == nil {
         resolver, err := selector.SelectorFieldResolver(q.dest)
         if err != nil {
+            // Log the error before falling back to EmptyResolver
+            log.Printf("Failed to initialize selector field resolver: %v", err)
             q.resolver = selector.EmptyResolver{}
         } else {
             q.resolver = resolver
         }
     }
     return q
 }
internal/store/device.go (1)

170-213: Consider adding a DELETE trigger for complete synchronization.

While the current triggers handle INSERT and UPDATE operations effectively, consider adding a DELETE trigger to automatically clean up the device_labels table when a device is deleted. Although the foreign key with ON DELETE CASCADE would handle this, having an explicit trigger provides better visibility and control over the cleanup process.

 		CREATE TRIGGER device_labels_update
 		AFTER UPDATE OF labels ON devices
 		FOR EACH ROW
 		WHEN (OLD.labels IS DISTINCT FROM NEW.labels)
 		EXECUTE FUNCTION sync_device_labels();
+
+		CREATE TRIGGER device_labels_delete
+		BEFORE DELETE ON devices
+		FOR EACH ROW
+		EXECUTE FUNCTION (
+			DELETE FROM device_labels
+			WHERE org_id = OLD.org_id AND device_name = OLD.name;
+			RETURN OLD;
+		);
internal/api/client/client.gen.go (1)

1-11059: The client code changes align well with the device labels normalization effort.

The generated client code provides a clean interface for the new device labels endpoint, maintaining consistency with the existing patterns. The implementation properly handles all standard response cases and error scenarios.

api/v1alpha1/openapi.yaml (2)

16-32: Define Global Tag for device-labels
The GET operation for /api/v1/devicelabels is tagged with device-labels. However, this tag is not currently defined in the global tags section at the top of the specification. Please add a global tag definition (e.g., with a description such as "Operations for managing device labels") to ensure consistency and clarity in the API documentation.


4213-4221: Review Response Schema for Device Labels
The DeviceLabelMap schema returns an object (map) of distinct labels where each key is a label name and its corresponding value is a label value. Please verify that this format meets the intended use case. If there is a possibility that multiple labels might share the same key (or if clients require access to more than one value per label key), consider revising the response schema to return an array of key–value pairs instead.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb3715e and 22950f7.

📒 Files selected for processing (22)
  • api/v1alpha1/openapi.yaml (2 hunks)
  • api/v1alpha1/spec.gen.go (1 hunks)
  • api/v1alpha1/types.gen.go (2 hunks)
  • internal/api/client/client.gen.go (7 hunks)
  • internal/api/server/server.gen.go (7 hunks)
  • internal/service/device.go (1 hunks)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/resource.go (1 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • internal/store/selector/label_test.go
  • internal/store/model/device.go
  • internal/store/selector/annotation_test.go
  • internal/store/selector/label.go
  • internal/store/selector/field_test.go
  • internal/store/selector/selector_test.go
  • internal/store/selector/types.go
  • internal/store/model/resource.go
  • pkg/k8s/selector/selector.go
🧰 Additional context used
📓 Learnings (1)
api/v1alpha1/openapi.yaml (1)
Learnt from: asafbss
PR: flightctl/flightctl#845
File: api/v1alpha1/openapi.yaml:925-981
Timestamp: 2025-02-04T07:53:53.903Z
Learning: The /api/v1/devicelabels endpoint in PR #845 is temporarily exposed for solution validation purposes only, with its final API design still to be determined.
🪛 GitHub Actions: API Quality
api/v1alpha1/openapi.yaml

[error] 952-952: Verb /api/v1/devicelabels.post is required.


[error] 952-952: Verb /api/v1/devicelabels.delete is required.


[warning] 955-955: Operation tags must be defined in global tags.

⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: smoke
  • GitHub Check: e2e
  • GitHub Check: integration-tests
  • GitHub Check: lint
  • GitHub Check: unit-tests
🔇 Additional comments (25)
internal/store/selector/resolvers.go (2)

10-26: LGTM! Clean implementation of a no-op resolver.

The EmptyResolver provides a clean and correct implementation of the Resolver interface, returning empty slices for all methods.


28-56: LGTM! Clean initialization of composite resolver.

The NewCompositeSelectorResolver function correctly:

  • Initializes the resolver map
  • Parses schema to retrieve table names
  • Handles errors from schema parsing and field resolution
internal/store/model/selectors.go (1)

72-80: LGTM! Clean implementation of selector name mapping.

The method correctly maps the composite selector to its constituent parts using case-insensitive comparison.

internal/store/common.go (2)

58-64: LGTM! Clean implementation of options pattern.

The implementation follows Go best practices for functional options pattern.


134-143: LGTM! Clean implementation of field resolution with fallback.

The method correctly handles field resolution and provides appropriate fallback behavior.

internal/store/selector/annotation.go (1)

126-188: LGTM! Clean implementation of resolver-based parsing.

The changes correctly:

  • Validate the resolver parameter
  • Handle errors comprehensively
  • Provide clear documentation with examples
internal/store/selector/selector.go (1)

1-352: LGTM!

The changes to the selectorFieldResolver functionality improve error handling and the public interface. The code is well-structured and follows best practices.

api/v1alpha1/spec.gen.go (1)

1-319: No review needed for auto-generated code.

This file contains auto-generated code from the OpenAPI specification.

internal/store/selector/field.go (3)

29-32: LGTM!

The new selectorParserSession type improves type safety and maintainability by encapsulating the selector and resolver.


429-435: LGTM!

The UUID handling implementation is robust with proper validation and parsing.


191-195: LGTM!

The updated Parse method improves type safety and error handling by requiring a non-nil resolver.

internal/store/device.go (2)

78-78: LGTM: Device labels table migration added correctly.

The migration for the new DeviceLabel model is properly added alongside the existing Device model migration.


156-168: LGTM: Efficient indexing strategy for device labels.

The implementation:

  • Properly enables pg_trgm extension for partial matching
  • Creates a GIN index supporting both label_key and label_value fields
  • Includes appropriate existence checks to prevent duplicate indexes
api/v1alpha1/types.gen.go (1)

1464-1474: LGTM: Well-structured parameters for listing device labels.

The ListDeviceLabelsParams struct is properly defined with:

  • Optional label and field selectors for filtering
  • Limit parameter for pagination
  • Appropriate JSON and form field tags
internal/api/server/server.gen.go (6)

51-52: LGTM: Interface definition for device labels endpoint.

The new ListDeviceLabels method is correctly added to the ServerInterface with appropriate parameters.


271-274: LGTM: Unimplemented stub handler.

The unimplemented handler correctly returns HTTP 501 Not Implemented status, following the pattern used for other endpoints.


3239-3290: LGTM: Request/Response type definitions.

The request and response types are properly defined with:

  • Request object containing query parameters
  • Response object interface with visitor pattern
  • Response implementations for success (200) and error cases (400, 401, 403, 503)

823-865: LGTM: Handler implementation.

The middleware handler implementation follows the established pattern with:

  • Proper context handling
  • Parameter binding from query string
  • Error handling for invalid parameters
  • Middleware chain execution
  • Response type validation

2516-2518: LGTM: Router configuration.

The new endpoint is correctly registered with the router using the chi framework.


1-8550: Verify generated code integrity.

Since this is generated code, let's verify it matches the OpenAPI spec.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Description: Verify the generated code matches the OpenAPI spec

# Check if the OpenAPI spec defines the devicelabels endpoint
rg -A 5 '/api/v1/devicelabels' api/v1alpha1/openapi.yaml

# Check if the spec defines the DeviceLabelMap type
rg 'DeviceLabelMap' api/v1alpha1/openapi.yaml

Length of output: 324


Generated Code Verified Against OpenAPI Spec Integrity

The search results confirm that the OpenAPI spec defines the "/api/v1/devicelabels" endpoint (with a GET operation) and references the DeviceLabelMap type as expected. The generated code matches these definitions.

internal/api/client/client.gen.go (4)

131-132: LGTM: Interface addition for device labels listing.

The new ListDeviceLabels method is correctly added to the ClientInterface with appropriate parameters.


511-521: LGTM: Client implementation for device labels listing.

The implementation follows the standard pattern used throughout the codebase:

  • Proper error handling
  • Context propagation
  • Request editor application

5070-5094: LGTM: Response type definition for device labels.

The ListDeviceLabelsResponse struct correctly handles:

  • Raw response body
  • HTTP response object
  • JSON response for success (200) and error cases (400, 401, 403, 503)

7867-7918: LGTM: Response parsing implementation.

The ParseListDeviceLabelsResponse function properly:

  • Reads and closes response body
  • Handles content type checking
  • Unmarshals JSON responses for all status codes
api/v1alpha1/openapi.yaml (1)

952-1008: ⚠️ Potential issue

Missing Required HTTP Methods in /api/v1/devicelabels Endpoint
The newly introduced /api/v1/devicelabels path currently only defines a GET operation. However, API quality checks (and previous design discussions) require that POST and DELETE operations be defined as well for complete CRUD support. Even if this endpoint is temporary and used solely for validation purposes, please add stub implementations (or at least placeholders with appropriate documentation/TODO markers) for the POST and DELETE verbs.
For example, you might add something like:

‾‾‾diff

  • post:
  • tags:
  •  - device-labels
    
  • description: Create a new device label mapping entry (temporary stub – final design TBD).
  • operationId: createDeviceLabel
  • requestBody:
  •  description: [Temporary] Device label mapping entry.
    
  •  content:
    
  •    application/json:
    
  •      schema:
    
  •        $ref: '#/components/schemas/DeviceLabelMap'
    
  •  required: true
    
  • responses:
  •  "201":
    
  •    description: Created
    
  •    content:
    
  •      application/json:
    
  •        schema:
    
  •          $ref: '#/components/schemas/DeviceLabelMap'
    
  •  "400":
    
  •    description: Bad Request
    
  •    content:
    
  •      application/json:
    
  •        schema:
    
  •          $ref: '#/components/schemas/Error'
    
  • delete:
  • tags:
  •  - device-labels
    
  • description: Delete device label mapping entries (temporary stub – final design TBD).
  • operationId: deleteDeviceLabels
  • parameters:
  •  - name: labelSelector
    
  •    in: query
    
  •    description: Selector to restrict which device label mappings to delete.
    
  •    schema:
    
  •      type: string
    
  • responses:
  •  "200":
    
  •    description: OK
    
  •    content:
    
  •      application/json:
    
  •        schema:
    
  •          $ref: '#/components/schemas/Status'
    
  •  "400":
    
  •    description: Bad Request
    
  •    content:
    
  •      application/json:
    
  •        schema:
    
  •          $ref: '#/components/schemas/Error'
    

‾‾‾

🧰 Tools
🪛 GitHub Actions: API Quality

[error] 952-952: Verb /api/v1/devicelabels.post is required.


[error] 952-952: Verb /api/v1/devicelabels.delete is required.


[warning] 955-955: Operation tags must be defined in global tags.

Comment thread internal/service/device.go Outdated
Comment thread internal/store/device.go Outdated
Comment thread api/v1alpha1/types.gen.go Outdated
@asafbss
Copy link
Copy Markdown
Collaborator Author

asafbss commented Feb 9, 2025

For testing, I’ve created a temporary endpoint (api/v1/devicelabels). While its final design is still TBD, it will allow you to verify the functionality. Please note that this will not be implemented via CLI until we reach a consensus. Also, only new
Example query:

/api/v1/devicelabels?fieldSelector=metadata.label.keyorvalue%20contains%20bla&limit=10
Let me know once you've confirmed.

Hi, @asafbss I've tested the new endpoint, and I see a problem in its response type. As it returns a Map, it cannot return the list of all labels which match the query if they share the exact key. This is an issue as any user that only remembers the key won't be able to find the labels they are looking for.
Eg. The following requests does not return all labels that include "test" because there are multiple labels with a key equal to "test". (This happens either with metadata.label.key=test AND metadata.label.keyorvalue=test)

curl -k -X 'GET' 'https://api.192.168.1.132.nip.io:3443/api/v1/devicelabels?fieldSelector=metadata.label.key=test&limit=10' -H 'accept: application/json' -H "Authorization: Bearer $TOKEN"
{"test":"yes"}

I'd expect the response to be a List of unique labels matching the query (either as key=value strings or as `{key: 'just-a-test', value: 'yest'} objects

["just-a-test=yes", "testing=yes", "testing=no", "this-is=a-test", "test"]

devices-labels-endpoint-test.mp4

Good catch, I'll return an Array instead of Map. Thanks 🙏

Go head, try again :)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
api/v1alpha1/types.gen.go (1)

419-420: 🛠️ Refactor suggestion

Consider using a structured type for device labels.

The current implementation using a string slice with "key=value" format has limitations:

  1. No validation of key/value format
  2. Requires parsing strings for key-value operations
  3. Cannot represent multiple distinct label combinations with the same key

Consider using a structured type instead:

-// DeviceLabelList A list of distinct labels, where each item is formatted as "key=value".
-type DeviceLabelList = []string
+// DeviceLabel represents a single key-value label pair
+type DeviceLabel struct {
+    Key   string `json:"key"`
+    Value string `json:"value"`
+}
+
+// DeviceLabelList A list of distinct labels
+type DeviceLabelList struct {
+    Items []DeviceLabel `json:"items"`
+}
🧹 Nitpick comments (3)
internal/store/selector/resolvers.go (1)

93-107: Optimize slice allocation in List method.

Pre-allocate the slice with the expected capacity to avoid potential reallocations.

 func (r *CompositeSelectorResolver) List() []SelectorName {
-	set := NewSelectorFieldNameSet()
+	// Pre-allocate with capacity based on number of resolvers
+	set := NewSelectorFieldNameSet()
+	list := make([]SelectorName, 0, len(r.resolvers))
 
 	for _, resolver := range r.resolvers {
 		set.Add(resolver.List()...)
 	}
 
-	list := set.List()
+	list = set.List()
 	sort.Slice(list, func(i, j int) bool {
 		return list[i].String() < list[j].String()
 	})
internal/store/common.go (1)

134-143: Consider adding error logging in resolveOrDefault.

The method silently falls back to the default value when resolution fails. Consider logging the error for debugging purposes.

 func (lq *listQuery) resolveOrDefault(sn selector.SelectorName, d string) string {
 	r, err := lq.resolver.ResolveFields(sn)
 	if err != nil {
+		// Log error for debugging purposes
+		log.Printf("Failed to resolve field %s: %v, using default value %s", sn, err, d)
 		return d
 	}
 	if len(r) > 0 && r[0] != nil {
 		return r[0].FieldName
 	}
+	log.Printf("No fields resolved for %s, using default value %s", sn, d)
 	return d
 }
internal/store/device.go (1)

237-271: Consider returning structured label data.

The current implementation returns labels as string slices in "key=value" format. Consider returning a structured type to make it easier for clients to parse and use the data.

Apply this diff to return structured data:

-func (s *DeviceStore) Labels(ctx context.Context, orgId uuid.UUID, listParams ListParams) ([]string, error) {
+func (s *DeviceStore) Labels(ctx context.Context, orgId uuid.UUID, listParams ListParams) ([]struct{ Key, Value string }, error) {
     var labels []model.DeviceLabel
 
     if listParams.Limit < 0 {
         return nil, flterrors.ErrLimitParamOutOfBounds
     }
 
     resolver, err := selector.NewCompositeSelectorResolver(&model.Device{}, &model.DeviceLabel{})
     if err != nil {
         return nil, fmt.Errorf("failed to create selector resolver: %w", err)
     }
 
     query, err := ListQuery(model.Device{}, WithSelectorResolver(resolver)).BuildNoOrder(ctx, s.db, orgId, listParams)
     if err != nil {
         return nil, err
     }
 
     query = query.Select("DISTINCT device_labels.label_key, device_labels.label_value").
         Joins("JOIN device_labels ON devices.org_id = device_labels.org_id AND devices.name = device_labels.device_name")
 
     if listParams.Limit > 0 {
         query = query.Limit(listParams.Limit)
     }
 
     if err := query.Find(&labels).Error; err != nil {
         return nil, ErrorFromGormError(err)
     }
 
-    labelStrings := make([]string, len(labels))
-    for i, label := range labels {
-        labelStrings[i] = fmt.Sprintf("%s=%s", label.LabelKey, label.LabelValue)
-    }
-
-    return labelStrings, nil
+    result := make([]struct{ Key, Value string }, len(labels))
+    for i, label := range labels {
+        result[i] = struct{ Key, Value string }{
+            Key:   label.LabelKey,
+            Value: label.LabelValue,
+        }
+    }
+
+    return result, nil
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22950f7 and 052c630.

📒 Files selected for processing (21)
  • api/v1alpha1/openapi.yaml (2 hunks)
  • api/v1alpha1/spec.gen.go (1 hunks)
  • api/v1alpha1/types.gen.go (2 hunks)
  • internal/api/client/client.gen.go (7 hunks)
  • internal/api/server/server.gen.go (7 hunks)
  • internal/service/device.go (1 hunks)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/store/selector/label_test.go
  • internal/store/model/device.go
  • internal/store/selector/label.go
  • internal/store/selector/selector_test.go
  • internal/store/selector/types.go
  • pkg/k8s/selector/selector.go
🧰 Additional context used
📓 Learnings (1)
api/v1alpha1/openapi.yaml (1)
Learnt from: asafbss
PR: flightctl/flightctl#845
File: api/v1alpha1/openapi.yaml:925-981
Timestamp: 2025-02-04T07:53:53.903Z
Learning: The /api/v1/devicelabels endpoint in PR #845 is temporarily exposed for solution validation purposes only, with its final API design still to be determined.
🪛 GitHub Actions: API Quality
api/v1alpha1/openapi.yaml

[error] 952-952: Verb /api/v1/devicelabels.post is required.


[error] 952-952: Verb /api/v1/devicelabels.delete is required.


[warning] 955-955: Operation tags must be defined in global tags.

⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: e2e
  • GitHub Check: integration-tests
  • GitHub Check: smoke
  • GitHub Check: lint
  • GitHub Check: unit-tests
🔇 Additional comments (35)
internal/store/selector/resolvers.go (2)

10-26: LGTM! Clean implementation of the null object pattern.

The EmptyResolver provides a safe no-op implementation that can be used as a default resolver.


28-56: LGTM! Well-structured composite pattern implementation.

The CompositeSelectorResolver effectively manages multiple resolvers with proper error handling and clear separation of concerns.

internal/store/selector/annotation_test.go (1)

25-29: LGTM! Comprehensive test coverage.

The test cases thoroughly validate the resolver functionality, error handling, and various selector operations.

Also applies to: 83-87, 161-165

internal/store/model/selectors.go (1)

72-84: LGTM! Well-implemented selector functionality for DeviceLabel.

The implementation follows established patterns and adds useful search flexibility with the keyorvalue selector.

internal/store/common.go (1)

58-88: LGTM! Clean implementation of the functional options pattern.

The ListQueryOption and WithSelectorResolver provide a flexible and extensible way to configure queries.

internal/store/selector/annotation.go (3)

127-134: LGTM! Documentation improvements.

The updated documentation clearly describes the method's parameters and their purpose.


149-153: LGTM! Proper error handling.

Good addition of nil check for the resolver parameter.


155-156: LGTM! Clean resolver usage.

The code now properly uses the resolver interface for field resolution.

internal/store/selector/selector.go (3)

108-110: LGTM! Proper handling of partial field resolution.

Good addition to reset fields when not all selectors are resolved, ensuring data consistency.


115-206: LGTM! Well-structured resolver implementation.

The new resolveSelector method provides comprehensive field resolution with proper error handling for:

  • Schema type validation
  • Array validation
  • JSONB field handling
  • Nested selector resolution

227-231: LGTM! Improved hidden selector filtering.

Good use of slices.DeleteFunc to filter out hidden selectors from the list.

internal/service/device.go (3)

134-183: Consider returning an array instead of a map for device labels.

Based on user feedback in the PR comments, returning a map prevents users from retrieving all labels that match a query if they share the same key. Consider modifying the response to return a list of unique labels matching the query instead.


156-166: LGTM! Proper limit handling.

Good implementation of limit validation and default value assignment.


175-182: LGTM! Comprehensive error handling.

Good error handling for:

  • Limit parameter bounds
  • Selector errors
internal/store/selector/field_test.go (3)

33-34: LGTM! Comprehensive UUID test cases.

Good addition of both valid and invalid UUID test cases.

Also applies to: 46-47


101-107: LGTM! Complete UUID operation test coverage.

Good coverage of UUID operations including:

  • Existence checks
  • Equality operations
  • Array operations

200-202: LGTM! Proper UUID validation.

Good addition of test cases to verify that contains/notcontains operations are not supported for UUIDs.

api/v1alpha1/spec.gen.go (1)

21-246: LGTM!

The changes to the swagger specification are auto-generated and align with the API changes for device labels management.

internal/store/selector/field.go (4)

29-32: LGTM! Well-structured session type.

The new selectorParserSession type effectively encapsulates the selector and resolver, improving code organization and type safety.


191-195: Improved error handling for resolver validation.

Early validation of the resolver parameter prevents potential nil pointer dereferences and provides clear error messages.


429-435: LGTM! Robust UUID value parsing.

The implementation correctly handles UUID parsing and validation using the google/uuid package.


529-541: LGTM! Comprehensive UUID operator support.

The applyUUIDOperator function supports all necessary comparison operators for UUID fields while properly rejecting unsupported operations.

internal/store/device.go (2)

156-168: LGTM! Efficient indexing for partial matching.

The implementation correctly creates GIN indexes with trigram operators for partial matching on label keys and values, which will improve search performance.


170-213: Verify the trigger implementation.

The trigger implementation for synchronizing device labels looks correct, but let's verify a few aspects:

  1. The trigger handles both INSERT and UPDATE operations
  2. The trigger properly cleans up old labels
  3. The trigger maintains data consistency with UPSERT operations

Run this script to verify the trigger implementation:

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Description: Verify the trigger implementation for device labels synchronization

# Test 1: Check if triggers are created
echo "Checking triggers..."
rg -A 5 "CREATE TRIGGER device_labels" internal/store/device.go

# Test 2: Check if the trigger function handles all cases
echo "Checking trigger function..."
rg -A 20 "sync_device_labels" internal/store/device.go

# Test 3: Check if proper cleanup is implemented
echo "Checking cleanup logic..."
rg "DELETE FROM device_labels" internal/store/device.go

Length of output: 2558


Verification of Trigger Implementation

  • Both INSERT and UPDATE triggers are correctly created and reference the "sync_device_labels" function.
  • The trigger function properly performs cleanup by deleting outdated labels during UPDATE events.
  • The UPSERT operation using "ON CONFLICT" ensures data consistency.
api/v1alpha1/types.gen.go (1)

1464-1474: LGTM! Well-structured parameters for listing device labels.

The parameters follow Kubernetes conventions for list operations and include:

  • Label selector for filtering by labels
  • Field selector for filtering by fields
  • Limit for pagination
internal/api/server/server.gen.go (5)

51-52: LGTM: New interface method for device labels endpoint.

The interface definition for ListDeviceLabels is correctly added to the ServerInterface with appropriate parameters.


271-274: LGTM: Unimplemented stub handler added.

The unimplemented stub handler for ListDeviceLabels follows the same pattern as other endpoints, returning StatusNotImplemented by default.


823-865: LGTM: Handler implementation follows established patterns.

The ListDeviceLabels handler implementation:

  • Correctly handles context and error management
  • Properly binds query parameters (labelSelector, fieldSelector, limit)
  • Follows the same middleware pattern as other endpoints
  • Has appropriate error handling for parameter binding

2516-2518: LGTM: Router configuration properly added.

The new endpoint is correctly registered in the router configuration with appropriate path and handler.


1-8550: 🏁 Script executed:

#!/bin/bash
# Description: Verify the generated code matches the OpenAPI spec

# Find the OpenAPI spec file
fd -t f 'openapi.yaml' -x echo "Found OpenAPI spec at: {}"

# Check when the spec was last modified
fd -t f 'openapi.yaml' -x stat -f "%m - %N" {}

# Check if there are any uncommitted changes to the spec
git status --porcelain | grep 'openapi.yaml'

Length of output: 332

internal/api/client/client.gen.go (4)

131-132: LGTM! Interface method addition is consistent.

The ListDeviceLabels method follows the established pattern for list operations in the interface.


511-521: LGTM! Client method implementation is correct.

The implementation follows the established pattern for list operations, with proper context handling and error management.


5070-5094: LGTM! Response type definition is complete.

The ListDeviceLabelsResponse struct includes all necessary fields and methods for handling the response, with proper error types.


7867-7918: LGTM! Response parser implementation is thorough.

The parser correctly handles all response types and follows proper response body handling practices.

api/v1alpha1/openapi.yaml (1)

952-1007: undefined

🧰 Tools
🪛 GitHub Actions: API Quality

[error] 952-952: Verb /api/v1/devicelabels.post is required.


[error] 952-952: Verb /api/v1/devicelabels.delete is required.


[warning] 955-955: Operation tags must be defined in global tags.

@celdrake
Copy link
Copy Markdown
Contributor

celdrake commented Feb 10, 2025

@asafbss Tested with the latest changes and on a first glance, I can see it's working correctly 🎉 . I won't be able to spend much more time on it though.

EDIT: I just noticed that unlike other "List" responses, there is no remainingItemCount or continue token when one specifies the limit. Probably it's fine, as I don't expect there would be hundreds of labels with similar text.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
internal/store/common.go (1)

134-143: Consider optimizing the field resolution logic.

The current implementation could be more efficient by avoiding unnecessary array access.

Consider this optimization:

-func (lq *listQuery) resolveOrDefault(sn selector.SelectorName, d string) string {
-	r, err := lq.resolver.ResolveFields(sn)
-	if err != nil {
-		return d
-	}
-	if len(r) > 0 && r[0] != nil {
-		return r[0].FieldName
-	}
-	return d
-}
+func (lq *listQuery) resolveOrDefault(sn selector.SelectorName, d string) string {
+	names, err := lq.resolver.ResolveNames(sn)
+	if err != nil || len(names) == 0 {
+		return d
+	}
+	return names[0]
+}

This version:

  1. Uses ResolveNames which is more appropriate for this use case
  2. Avoids unnecessary array access checks
internal/store/device.go (1)

170-213: Add a comment about PostgreSQL dependency.

The trigger implementation for synchronizing device labels is well-designed. Consider adding a comment to document that this synchronization mechanism is PostgreSQL-specific.

 // Ensure trigger is created for INSERT & UPDATE (labels JSONB changes)
+// Note: This synchronization mechanism is PostgreSQL-specific and relies on PL/pgSQL triggers
 if s.db.Dialector.Name() == "postgres" {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 052c630 and 6922bd9.

📒 Files selected for processing (19)
  • api/v1alpha1/openapi.yaml (1 hunks)
  • api/v1alpha1/spec.gen.go (2 hunks)
  • api/v1alpha1/types.gen.go (1 hunks)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/resource.go (1 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • api/v1alpha1/types.gen.go
  • internal/store/selector/label_test.go
  • internal/store/selector/field_test.go
  • internal/store/selector/annotation_test.go
  • internal/store/model/device.go
  • internal/store/selector/selector_test.go
  • internal/store/selector/label.go
  • internal/store/model/resource.go
  • internal/store/selector/resolvers.go
  • pkg/k8s/selector/selector.go
  • api/v1alpha1/openapi.yaml
⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: lint
  • GitHub Check: smoke
  • GitHub Check: e2e
  • GitHub Check: integration-tests
  • GitHub Check: unit-tests
🔇 Additional comments (22)
internal/store/model/selectors.go (2)

72-80: LGTM! Clean implementation of selector name mapping.

The function correctly implements case-insensitive mapping of the composite selector to its constituent parts.


82-84: LGTM! Clean implementation of selector listing.

The function correctly returns a selector set containing the composite selector.

internal/store/common.go (3)

58-64: LGTM! Clean implementation of the functional options pattern.

The implementation provides a flexible and type-safe way to configure listQuery instances.


71-88: LGTM! Robust initialization with fallback handling.

The function correctly handles option application and provides a sensible default resolver.


90-124: LGTM! Clean integration of resolver-based field resolution.

The method correctly uses the resolver for field resolution while maintaining existing functionality.

internal/store/selector/types.go (3)

14-14: LGTM! Clean addition of UUID support.

The UUID constant is correctly integrated into the type system.


67-79: LGTM! Well-designed and documented interface.

The Resolver interface is well-structured with clear method responsibilities and comprehensive documentation.


175-176: LGTM! Consistent type string representation.

The UUID string representation follows the established pattern and matches the database type.

internal/store/selector/annotation.go (2)

126-149: LGTM! Clear and comprehensive documentation update.

The documentation accurately reflects the new resolver-based approach with clear examples.


149-188: LGTM! Robust implementation with comprehensive error handling.

The implementation correctly uses the resolver with appropriate error handling at each step.

internal/store/selector/selector.go (4)

29-32: LGTM!

The selectorParserSession struct is well-designed with clear and descriptive field names.


31-45: LGTM!

The function signature change to return Resolver interface improves the abstraction and aligns with the PR objectives.


108-110: LGTM!

The added condition is a good defensive programming practice that ensures data consistency by resetting fields when lengths don't match.


115-206: LGTM!

The resolveSelector function is well-implemented with:

  • Clear error handling
  • Proper array indexing and JSONB casting
  • Descriptive error messages
internal/store/selector/field.go (2)

429-435: LGTM!

The UUID value resolution is correctly implemented with proper parsing and validation.


529-541: LGTM!

The UUID operator handling is comprehensive with support for all standard comparison operators and proper error handling. Unit tests for this functionality exist in field_test.go.

internal/store/device.go (3)

78-78: LGTM!

The addition of model.DeviceLabel to AutoMigrate aligns with the PR objectives to normalize device labels into a separate table.


156-168: LGTM!

The GIN index creation with pg_trgm extension will optimize partial match searches on label keys and values.


237-271: LGTM!

The Labels method implementation correctly returns a slice of strings to preserve all distinct label combinations, avoiding potential data loss that could occur with a map-based approach.

api/v1alpha1/spec.gen.go (3)

1-16: LGTM! Note that this is generated code.

The imports are appropriate for handling the encoded Swagger specification. As this is generated code, any changes should be made to the source OpenAPI specification rather than this file.


246-316: LGTM! The utility functions are well-implemented.

The functions for decoding and loading the specification handle errors appropriately, implement efficient caching, and securely resolve external references.


19-244: Verify the encoded Swagger specification content.

The Base64 encoded specification has been updated to include the new device label endpoints and schemas. Let's verify the decoded content matches the expected changes.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Description: Decode and verify the Swagger specification content

# Test: Decode the Base64 content and check for device label related endpoints
echo "$(echo "$(echo "${swaggerSpec[*]}" | base64 -d)" | gunzip)" | grep -A 10 "devicelabels"

Length of output: 216


Additional verification required for the encoded spec.

The initial command failed because the environment didn’t have the base64 utility. Please run the following Python‐based script to decode the Swagger specification from
api/v1alpha1/spec.gen.go and verify whether it contains the expected "devicelabel" endpoints. Once confirmed, we can conclude the review items.

Once this script is executed, please verify that the output confirms the presence of the new device label endpoints and schema changes as expected by the PR objectives.


🏁 Script executed:

#!/bin/bash
# This script uses Python to decode and decompress the swaggerSpec variable from the Go file
python3 << 'EOF'
import re, base64, gzip

try:
    with open("api/v1alpha1/spec.gen.go", "r") as f:
        content = f.read()
except Exception as e:
    print("Error reading file:", e)
    exit(1)

# Extract the swaggerSpec block containing the Base64 encoded, gzipped content
m = re.search(r'var\s+swaggerSpec\s*=\s*\[]string\s*{([^}]+)}', content, re.DOTALL)
if not m:
    print("swaggerSpec variable block not found.")
    exit(1)

# Find all base64 encoded string literals in the block
strings = re.findall(r'"([^"]+)"', m.group(1))
if not strings:
    print("No base64 encoded strings found.")
    exit(1)

# Concatenate all parts to form the full Base64 string
data = "".join(strings)

try:
    decoded = base64.b64decode(data)
except Exception as e:
    print("Error decoding base64:", e)
    exit(1)

try:
    decompressed = gzip.decompress(decoded).decode("utf-8")
except Exception as e:
    print("Error decompressing:", e)
    exit(1)

# Look for device label related endpoints (case insensitive match)
matches = [line for line in decompressed.splitlines() if "devicelabel" in line.lower()]
if matches:
    print("Device label endpoints found:")
    for line in matches:
        print(line)
else:
    print("No device label endpoints found in the decoded spec.")
EOF

Length of output: 65546


The decoded Swagger spec confirms the updated device label endpoints and schema changes.

The Python script output shows that the spec now contains device label related definitions (e.g., "DeviceLabelList"), aligning with the PR objectives.

@asafbss asafbss enabled auto-merge (squash) February 10, 2025 10:24
@asafbss
Copy link
Copy Markdown
Collaborator Author

asafbss commented Feb 10, 2025

@asafbss Tested with the latest changes and on a first glance, I can see it's working correctly 🎉 . I won't be able to spend much more time on it though.

EDIT: I just noticed that unlike other "List" responses, there is no remainingItemCount or continue token when one specifies the limit. Probably it's fine, as I don't expect there would be hundreds of labels with similar text.

Correct, pagination for this list is not supported and will have a hard limit (e.g., 1000) once the API is introduced.

…g JSONB for fast listing

- Introduce `device_labels` table to store labels as structured key-value pairs for optimized querying
- Retain `devices.labels` JSONB field to ensure efficient device listing while enabling structured lookups
- Ensure `device_labels` references `devices (org_id, name)` as a foreign key with `ON DELETE CASCADE`
- Improve query performance by indexing `label_key` and `label_value` using `pg_trgm` for partial matching
- Add trigger-based synchronization to keep `device_labels` updated whenever `devices.labels` JSONB changes
- Lay the foundation for efficient, indexed label-based queries without requiring full JSONB scans
- Implement `Labels` function in `DeviceStore` to retrieve distinct labels efficiently.
- Utilize `CompositeSelectorResolver` to dynamically resolve field names across `devices` and `device_labels`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
internal/store/selector/resolvers.go (1)

58-91: Potential improvement to logging errors.
When iteration encounters an error in ResolveNames or ResolveFields, it returns immediately. Consider adding logging or wrapping the error with additional context about which table triggered the error to improve debuggability.

internal/store/common.go (1)

71-88: Provide clearer error feedback when resolving fields fails.
By defaulting to EmptyResolver on error, potential misconfigurations silently degrade to no-op resolutions. You may want to log or bubble up such errors if they are likely to indicate a developer mistake.

internal/store/selector/types.go (1)

108-110: Consider adding a comment explaining the reset logic.

The reset of the fields slice when the length doesn't match selectorNames is a critical operation that deserves explanation.

+  // Reset fields if not all selectors were resolved to maintain consistency
   if len(fields) > 0 && len(fields) != len(selectorNames) {
     fields = make([]*SelectorField, 0)
   }
internal/store/selector/annotation.go (1)

149-153: Consider using a constant for the error message.

The error message for invalid array access could be moved to a constant to maintain consistency and make updates easier.

+ const errInvalidArrayAccess = "array access must specify a valid index (e.g., 'conditions[0]'); invalid selector: %s"

  if !arrayPattern.MatchString(selectorName) {
-   return nil, fmt.Errorf(
-     "array access must specify a valid index (e.g., 'conditions[0]'); invalid selector: %s", selectorName)
+   return nil, fmt.Errorf(errInvalidArrayAccess, selectorName)
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6922bd9 and 6930207.

📒 Files selected for processing (19)
  • api/v1alpha1/openapi.yaml (1 hunks)
  • api/v1alpha1/spec.gen.go (1 hunks)
  • api/v1alpha1/types.gen.go (1 hunks)
  • internal/store/common.go (2 hunks)
  • internal/store/device.go (5 hunks)
  • internal/store/model/device.go (2 hunks)
  • internal/store/model/resource.go (1 hunks)
  • internal/store/model/selectors.go (1 hunks)
  • internal/store/selector/annotation.go (2 hunks)
  • internal/store/selector/annotation_test.go (1 hunks)
  • internal/store/selector/field.go (10 hunks)
  • internal/store/selector/field_test.go (9 hunks)
  • internal/store/selector/label.go (3 hunks)
  • internal/store/selector/label_test.go (1 hunks)
  • internal/store/selector/resolvers.go (1 hunks)
  • internal/store/selector/selector.go (5 hunks)
  • internal/store/selector/selector_test.go (7 hunks)
  • internal/store/selector/types.go (4 hunks)
  • pkg/k8s/selector/selector.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • api/v1alpha1/types.gen.go
  • pkg/k8s/selector/selector.go
  • internal/store/selector/label_test.go
  • internal/store/selector/label.go
  • internal/store/model/device.go
  • internal/store/selector/annotation_test.go
  • internal/store/selector/field_test.go
  • internal/store/model/resource.go
  • internal/store/selector/selector_test.go
🧰 Additional context used
🧠 Learnings (1)
api/v1alpha1/openapi.yaml (1)
Learnt from: asafbss
PR: flightctl/flightctl#845
File: api/v1alpha1/openapi.yaml:925-981
Timestamp: 2025-02-04T07:53:53.903Z
Learning: The /api/v1/devicelabels endpoint in PR #845 is temporarily exposed for solution validation purposes only, with its final API design still to be determined.
⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-eln-aarch64
  • GitHub Check: lint
  • GitHub Check: e2e
  • GitHub Check: unit-tests
  • GitHub Check: integration-tests
  • GitHub Check: smoke
🔇 Additional comments (17)
internal/store/selector/resolvers.go (2)

28-31: Consider handling table name collisions.
Currently, if two models share the same table name, the last resolver assigned will overwrite any previous resolver in the resolvers map. If this is unintended, you might want to detect duplicates and raise an error or implement a merging strategy.


58-91: Return behavior in ResolveFields might cause partial resolution.
ResolveFields immediately returns once it finds non-empty fields in a single resolver. If a selector could validly match multiple resolvers, only the first match will be returned, ignoring others. Verify that this is the intended design.

internal/store/model/selectors.go (1)

72-85: Clarify combined selectors with “keyorvalue”.
The aggregator approach that returns both “metadata.label.key” and “metadata.label.value” is helpful for flexible matching. However, ensure that all query paths and the rest of your code can handle these two separate selectors consistently. For instance, confirm that your CompositeSelectorResolver or other resolvers do not mistakenly ignore “metadata.label.key” or “metadata.label.value” when referenced separately.

internal/store/common.go (2)

60-64: Injecting a custom resolver is a great addition.
The WithSelectorResolver function neatly encapsulates custom resolver injection. This will make the code more modular and testable.


134-143: Silent fallback in resolveOrDefault.
When ResolveFields fails, the function silently reverts to a default string. This can obscure potential incorrect selectors. Consider logging or clearly surfacing the error to guide queries toward valid selectors.

internal/store/selector/types.go (2)

14-14: LGTM! Addition of UUID type.

The addition of the UUID type to the SelectorType enum is well-placed and properly integrated with the existing type system.


67-79: LGTM! Well-designed Resolver interface.

The new Resolver interface is well-documented and provides a clean abstraction for dynamic selector resolution. The three methods cover all necessary operations:

  • ResolveNames: Maps selectors to field names
  • ResolveFields: Provides detailed selector metadata
  • List: Enumerates available selectors
internal/store/selector/selector.go (2)

31-31: LGTM! Interface change aligns with new architecture.

The return type change from *selectorFieldResolver to Resolver improves abstraction and flexibility.


227-231: LGTM! Improved hidden selector filtering.

The use of slices.DeleteFunc for filtering hidden selectors is a clean and efficient implementation.

api/v1alpha1/spec.gen.go (1)

21-243: LGTM! Updated API specification.

The swagger specification has been updated to include device label support, aligning with the PR objectives.

internal/store/selector/field.go (3)

29-32: LGTM! Good encapsulation of selector and resolver.

The introduction of selectorParserSession type and the resolver validation in Parse method improves type safety and error handling.

Also applies to: 191-195


429-435: LGTM! Comprehensive UUID support added.

The implementation includes proper UUID parsing, validation, and a complete set of supported operators (=, !=, >, >=, <, <=, in, notin, exists, !exists).

Also applies to: 529-541


643-650: LGTM! Improved error handling in resolveSelectorField.

The error message now includes the list of supported selectors, making it more helpful for debugging.

internal/store/device.go (3)

78-78: LGTM! Proper setup of device_labels table and indexes.

The migration correctly sets up the device_labels table and creates a GIN index for optimized partial match searches on label_key and label_value fields.

Also applies to: 156-168


170-213: LGTM! Well-implemented label synchronization trigger.

The trigger function correctly:

  • Synchronizes JSONB labels with the device_labels table
  • Handles both INSERT and UPDATE operations
  • Cleans up old labels on UPDATE
  • Uses ON CONFLICT for upsert behavior

237-271: LGTM! Efficient implementation of Labels method.

The method correctly:

  • Returns distinct label key-value pairs
  • Uses JOIN with device_labels table for efficient querying
  • Formats labels as "key=value" strings as required by the API
api/v1alpha1/openapi.yaml (1)

4157-4166: Review of the new DeviceLabelList schema definition

The new schema defines DeviceLabelList as an array of strings, where each entry is expected to be formatted as "key=value". This design meets the goal of returning an array (instead of a map) for distinct labels, which aligns with the feedback provided (i.e. ensuring that multiple labels sharing the same key can be returned correctly).

A few suggestions for further consideration:

  • Validation of Format: Consider if additional constraints (e.g., a regular expression) should be applied to ensure that each string adheres to the "key=value" format, especially to prevent issues if keys or values themselves contain an equals sign.
  • Documentation Clarity: The description and example are clear. However, you may want to note any limitations or expectations regarding special characters in keys/values.

Overall, the implementation looks consistent with the PR objectives and the API improvements discussed.

@asafbss asafbss merged commit 4579be4 into main Feb 10, 2025
@asafbss asafbss deleted the EDM-683 branch February 10, 2025 14:36
kubealex pushed a commit to kubealex/flightctl that referenced this pull request Apr 2, 2025
…Queries and Fast Distinct Label Retrieval (flightctl#845)

* EDM-683: Normalize device labels into a separate table while retaining JSONB for fast listing

- Introduce `device_labels` table to store labels as structured key-value pairs for optimized querying
- Retain `devices.labels` JSONB field to ensure efficient device listing while enabling structured lookups
- Ensure `device_labels` references `devices (org_id, name)` as a foreign key with `ON DELETE CASCADE`
- Improve query performance by indexing `label_key` and `label_value` using `pg_trgm` for partial matching
- Add trigger-based synchronization to keep `device_labels` updated whenever `devices.labels` JSONB changes
- Lay the foundation for efficient, indexed label-based queries without requiring full JSONB scans

* EDM-683: Add device Labels listing function and Composite Selector

- Implement `Labels` function in `DeviceStore` to retrieve distinct labels efficiently.
- Utilize `CompositeSelectorResolver` to dynamically resolve field names across `devices` and `device_labels`

* EDM-683: Add api.devicelist as preparation for future API

Signed-off-by: kubealex <al.rossi87@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants