Skip to content

Conversation

@benedeki
Copy link
Contributor

@benedeki benedeki commented Dec 17, 2025

Overview

  • Introduced a new class SqlEntry which represents a part of and SQL expression
  • SqlEntry brings a number of methods and helper classes
  • SqlEntryComposition offers a way to compose a full SQL expression as SqlEntry in SQL format (almost)
  • SqlEntry is now leveraged in SqlItem, DBFunction and DBTable

Release Notes

  • It's now easier and safer to work with SQL expression.
  • It's also possible to SQL expression in SQL-like way (almost) directly in Scala, not as strings
    • e.g. SELECT (all) FROM my_table WHERE condition ORDER BY (field1)

Related

Closes #62

Depends on #61

Summary by CodeRabbit

  • New Features

    • Fluent SQL DSL, SqlEntry wrapper, and typed column-reference utilities for safer query composition.
  • Refactor

    • Migrate SQL assembly and parameter handling to typed, position-aware containers and SqlEntry-backed builders.
    • Standardize APIs from "field/label" to "column" terminology.
  • Tests

    • Add unit and integration tests covering the DSL, SqlEntry, column-reference behavior and quoted/strange column names.
  • Chores

    • Add SQL test fixtures and CI steps to load them during database setup.

✏️ Tip: You can customize this high-level summary in your review settings.

* Introduced a new class `SqlEntry` which represents a part of and SQL expression
* `SqlEntry` brings a number of methods and helper classes
* `SqlEntryComposition` offers a way to compose a full SQL expression as `SqlEntry` in SQL format (almost)
* `SqlEntry` is now leveraged in `SqlItem`, `DBFunction` and `DBTable`
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

Adds a SqlEntry-based SQL composition DSL and ColumnReference types; replaces ParamsMap with typed NamedParams/OrderedParams; migrates DBFunction, DBTable, DBQuerySupport, QueryResultRow/QueryResult to use SqlEntry and typed params; adds tests and CI steps to load quoted/strange-column test data.

Changes

Cohort / File(s) Summary
CI Workflow Updates
​.github/workflows/build.yml, ​.github/workflows/jacoco_report.yml
Appended two psql commands to load balta/src/test/resources/db/postgres/10_testing.strange_columns.sql and .../11_testing.strange_columns_data.sql during DB setup.
SQL Composition Core
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala, .../SqlEntryComposition.scala, .../SqlItem.scala
New value-wrapper SqlEntry, fluent composition DSL (SqlEntryComposition) with SELECT/INSERT/DELETE/WHERE/ORDER/RETURNING fragments, and SqlItem trait.
Column Reference Abstraction
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala
New ColumnReference, ColumnName, ColumnIndex with quoting/escaping, normalization, equality/hash semantics.
Params & Parameter Model
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala
Replaced string-keyed ParamsMap with typed NamedParams (ColumnName keys) and OrderedParams (ColumnReference-indexed ParamsList); new accessors and factories.
QueryParamValue Updates
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala
QueryParamValue now exposes sqlEntry: SqlEntry and equalityOperator: SqlEntry; internal SQL tokens use SqlEntry.
DB Function / Table / Query Support
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala, .../DBTable.scala, .../DBQuerySupport.scala
Migrated SQL assembly to SqlEntry; DBFunction holds SqlEntry + OrderedParams/NamedParams; DBQuerySupport.runQuery accepts SqlEntry and Vector[QueryParamValue]; DBTable builds SQL via SqlEntryComposition.
QueryResult / Row Adjustments
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala, .../QueryResultRow.scala
Renamed FieldNamesColumnNames; metadata helper fieldNamesFromMetadatacolumnNamesFromMetadata; updated lookups and public signatures to use column-name semantics.
Implicits / Postgres Helpers
balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala, .../postgres/implicits/Postgres.scala
Renamed local/parameter identifiers from columnLabelcolumnName; preserved behavior.
Tests & Resources
balta/src/test/resources/db/postgres/10_testing.strange_columns.sql, .../11_testing.strange_columns_data.sql, balta/src/test/scala/...
Added DDL/DML for strange/quoted column names; new unit tests for ColumnReference, SqlEntry, SqlEntryComposition; integration tests updated / added to exercise quoted/special column handling.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Suite / Client
    participant DSL as SqlEntryComposition (DSL)
    participant DBLib as DBTable / DBFunction
    participant Runner as DBQuerySupport
    participant PG as Postgres DB

    Note over Test,DSL: Build query using DSL + ColumnReference
    Test->>DSL: compose SELECT ... FROM ... WHERE ... => SqlEntry
    DSL-->>DBLib: provide SqlEntry (query fragment)
    Test->>DBLib: attach NamedParams / OrderedParams
    DBLib->>Runner: runQuery(sql: SqlEntry, params: Vector[QueryParamValue])
    Runner->>Runner: prepareStatement(sql.entry) and bind params
    Runner->>PG: execute prepared statement
    PG-->>Runner: result set
    Runner->>DBLib: QueryResult (uses ColumnNames/QueryResultRow)
    DBLib-->>Test: invoke verification callback with QueryResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

breaking change

Suggested reviewers

  • lsulak
  • salamonpavel
  • miroslavpojer

Poem

🐰 I stitch SqlEntry threads with a twitch and a hop,

Quoted columns and accents I gently unstop.
Named nibs, ordered carrots in a tidy parade,
Tests nibble odd names in the shady glade.
Hop — the DSL hums; I leave a tiny code-glade.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introducing SqlEntry and SqlEntryComposition to simplify SQL expression composition.
Description check ✅ Passed The description covers the Overview, Release Notes, and Related sections as required by the template, providing clear context for the changes.
Linked Issues check ✅ Passed The PR code changes directly address issue #62 by introducing SqlEntry, SqlEntryComposition, and integrating them into DBFunction and DBTable as described.
Out of Scope Changes check ✅ Passed Changes to column naming conventions (fieldLabel → columnName) and parameter handling refactoring are aligned with the broader safety and clarity goals of making SQL composition easier and more idiomatic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/62-make-composition-of-sql-expression-easier-and-safer

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2065c and e60cfe5.

📒 Files selected for processing (1)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala
🚧 Files skipped from review as they are similar to previous changes (1)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@benedeki benedeki added the dependent The item depends on some other open item (Issue or PR) label Dec 17, 2025
@benedeki
Copy link
Contributor Author

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (1)

118-121: Incorrect deprecation message.

The deprecation message says "Use add(NULL)" but this method is addNull(paramName: String) which requires a parameter name. The replacement should be add(paramName, NULL) to maintain the same semantics.

-    @deprecated("Use add(NULL)", "balta 0.3.0")
+    @deprecated("Use add(paramName, NULL)", "balta 0.3.0")
     def addNull(paramName: String): NamedParams = {
       add(paramName, QueryParamType.NULL)
     }
🧹 Nitpick comments (11)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (1)

19-19: LGTM!

The rename from FieldNames to ColumnNames improves terminology consistency across the codebase. The change correctly updates both the import and the implicit field type.

Optional: Consider renaming the initialization method for consistency.

While not critical, the method fieldNamesFromMetadata could be renamed to columnNamesFromMetadata to align with the new terminology throughout the codebase.

Also applies to: 34-34

balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1)

56-56: Non-standard unapply signature.

The unapply method returns String directly instead of the conventional Option[String]. While this works as an irrefutable extractor, the standard Scala convention is to return Option[T] for single-value extraction. This affects usability in pattern guards and partial matching scenarios.

-  def unapply(columnName: ColumnName): String = columnName.enteredName
+  def unapply(columnName: ColumnName): Option[String] = Some(columnName.enteredName)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (3)

72-72: Unusual access modifier combination.

The protected modifier on a sealed class has limited utility since subclasses are constrained to this file anyway. Consider using just sealed for clarity.

-  sealed protected class Query(val sqlEntry: SqlEntry)
+  sealed class Query(val sqlEntry: SqlEntry)

80-80: Remove or address commented-out code.

The commented-out method creates technical debt. If this functionality is planned, consider adding a TODO with a tracking issue; otherwise, remove it.


116-116: Minor formatting inconsistency.

Missing space before = compared to other method definitions.

-  def BY(columns: ColumnReference*): OrderByFragment= new OrderByFragment(columnsToSqlEntry(columns))
+  def BY(columns: ColumnReference*): OrderByFragment = new OrderByFragment(columnsToSqlEntry(columns))
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (3)

33-33: Custom == operator shadows Any.==.

Defining def ==(other: String) can cause confusion since it shadows the inherited equality method. Callers might accidentally use sqlEntry == anotherSqlEntry expecting value comparison but getting reference comparison for non-String types.

Consider renaming to a more explicit method like equalsString or matches, or ensure documentation clarifies this behavior.


51-54: Implicit reliance on toString for SQL building.

The mkString call relies on SqlEntry.toString returning the underlying entry. While this works, using .map(_.sqlEntry.entry) would be more explicit and less fragile if toString were ever changed for debugging purposes.

   def apply(firstColumn: ColumnReference, otherColumns: ColumnReference*): SqlEntry = {
-    val allColumns = (firstColumn +: otherColumns).map(_.sqlEntry).mkString(", ")
+    val allColumns = (firstColumn +: otherColumns).map(_.sqlEntry.entry).mkString(", ")
     SqlEntry(allColumns)
   }

94-94: Typo in comment.

"so space" should be "no space".

-          // It looks better to have so space before characters like , ( )
+          // It looks better to have no space before characters like , ( )
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (3)

62-65: Verify NULL import guidance in deprecation messages.

The deprecation messages suggest add(paramName, NULL) and add(NULL), but users will need to reference QueryParamType.NULL unless they import it. Consider clarifying in the messages (e.g., "Use add(paramName, QueryParamType.NULL)") or providing a convenience re-export of NULL.

Also applies to: 86-89


125-130: Consider caching computed properties.

values, items, and paramNames are recomputed on each access via .toVector.map(...). For typical small parameter lists this is fine, but if these are accessed repeatedly, consider using lazy val or caching to avoid redundant allocations.

-    override def apply(position: Int): QueryParamValue = values(position)
-    override def size: Int =  namedParams.size
-    override def values: Vector[QueryParamValue] =  namedParams.toVector.map(_._2)
-    override def items: ParamsList = namedParams.toVector
-
-    def paramNames: Vector[ColumnName] = namedParams.toVector.map(_._1)
+    override lazy val items: ParamsList = namedParams.toVector
+    override lazy val values: Vector[QueryParamValue] = items.map(_._2)
+    lazy val paramNames: Vector[ColumnName] = items.map(_._1)
+    override def size: Int = namedParams.size
+    override def apply(position: Int): QueryParamValue = values(position)

149-153: Document the indexing convention.

ColumnReference is created with 1-based indices (line 150: size + 1), aligning with SQL conventions. However, apply(position: Int) uses 0-based indexing (line 167: items(position)). This is internally consistent but could confuse users. Consider adding a ScalaDoc clarifying that apply(position) uses 0-based indexing while the stored ColumnReference uses 1-based SQL-style indices.

Also applies to: 167-167

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d603b4 and 4e3f5c1.

📒 Files selected for processing (19)
  • .github/workflows/build.yml (1 hunks)
  • .github/workflows/jacoco_report.yml (1 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (7 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (11 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (2 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (2 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (7 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (2 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlItem.scala (1 hunks)
  • balta/src/test/resources/db/postgres/10_testing.strange_columns.sql (1 hunks)
  • balta/src/test/resources/db/postgres/11_testing.strange_columns_data.sql (1 hunks)
  • balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (2 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryUnitTests.scala (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (2)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
balta/src/main/scala/za/co/absa/db/mag/core/SqlItem.scala (2)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1)
  • sqlEntry (59-59)
balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryUnitTests.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (3)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
  • toString (45-45)
balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (3)
balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala (1)
  • test (65-81)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (6)
  • ColumnReference (34-67)
  • sqlEntry (59-59)
  • unapply (56-56)
  • unapply (71-71)
  • ColumnName (29-32)
  • ColumnName (69-72)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala (5)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (18)
  • SqlEntryComposition (5-120)
  • SELECT (111-111)
  • FROM (41-41)
  • FROM (49-49)
  • WHERE (76-76)
  • WHERE (77-79)
  • WHERE (92-92)
  • WHERE (93-95)
  • ORDER (58-58)
  • BY (114-114)
  • BY (115-115)
  • BY (116-116)
  • INSERT (112-112)
  • INTO (28-30)
  • VALUES (34-37)
  • RETURNING (63-65)
  • RETURNING (66-69)
  • DELETE (113-113)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (3)
  • ColumnName (29-32)
  • ColumnName (69-72)
  • sqlEntry (59-59)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (2)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala (1)
  • test (65-81)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (4)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (4)
  • sqlEntry (59-59)
  • apply (42-50)
  • apply (52-54)
  • apply (70-70)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (8)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
  • apply (35-39)
  • apply (50-50)
  • apply (51-54)
  • apply (56-56)
  • prefix (59-59)
  • toOption (41-43)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (4)
  • apply (144-146)
  • apply (148-150)
  • apply (152-154)
  • sql (41-49)
balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (1)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • NULL (86-86)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (7)
  • apply (35-39)
  • apply (50-50)
  • apply (51-54)
  • apply (56-56)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
  • toString (45-45)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (3)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (6)
  • ColumnName (29-32)
  • ColumnName (69-72)
  • ColumnReference (34-67)
  • apply (42-50)
  • apply (52-54)
  • apply (70-70)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
  • apply (59-59)
  • apply (65-65)
  • apply (157-160)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (16)
  • QueryParamType (32-92)
  • NULL (86-86)
  • toQueryParamValue (35-35)
  • toQueryParamValue (39-39)
  • toQueryParamValue (43-43)
  • toQueryParamValue (47-47)
  • toQueryParamValue (51-51)
  • toQueryParamValue (55-55)
  • toQueryParamValue (59-59)
  • toQueryParamValue (63-63)
  • toQueryParamValue (67-67)
  • toQueryParamValue (71-71)
  • toQueryParamValue (75-75)
  • toQueryParamValue (79-79)
  • toQueryParamValue (83-83)
  • toQueryParamValue (89-89)
balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (6)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • QueryParamType (32-92)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (3)
  • QueryParamValue (30-53)
  • sqlEntry (26-26)
  • equalityOperator (27-27)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (8)
  • Params (29-36)
  • Params (37-175)
  • OrderedParams (141-170)
  • OrderedParams (172-174)
  • values (127-127)
  • values (169-169)
  • paramNames (130-130)
  • items (128-128)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (5)
  • SqlEntry (23-47)
  • SqlEntry (49-103)
  • toOption (41-43)
  • toSqlEntry (69-71)
  • toSqlEntry (75-78)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (18)
  • SqlEntryComposition (5-120)
  • INSERT (112-112)
  • INTO (28-30)
  • VALUES (34-37)
  • RETURNING (63-65)
  • RETURNING (66-69)
  • SELECT (111-111)
  • FROM (41-41)
  • FROM (49-49)
  • WHERE (76-76)
  • WHERE (77-79)
  • WHERE (92-92)
  • WHERE (93-95)
  • ORDER (58-58)
  • BY (114-114)
  • BY (115-115)
  • BY (116-116)
  • DELETE (113-113)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)
  • runQuery (27-48)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (1)
balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala (1)
  • getOrThrow (28-30)
🪛 GitHub Actions: Build
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala

[error] 42-42: Postgres: syntax error at or near "id_field". Position: 38. (Cause: DBQuerySupport.runQuery)

balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala

[error] 1-1: sbt.TestsFailedException: Tests unsuccessful. 3 tests failed (DBTableIntegrationTests).

balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala

[error] 38-38: PostgreSQL syntax error at or near "id_field" during DBTable lifecycle tests. Position: 38.

🔇 Additional comments (24)
.github/workflows/build.yml (1)

74-75: LGTM!

The additional database setup commands follow the established pattern and correctly load the new test resources for strange column identifiers.

balta/src/test/resources/db/postgres/10_testing.strange_columns.sql (1)

17-33: LGTM!

The DDL effectively covers edge cases for column identifier handling: case sensitivity, spaces, special characters, Unicode, and numeric prefixes. This provides comprehensive test coverage for the new SQL composition framework.

.github/workflows/jacoco_report.yml (1)

51-52: LGTM!

The database setup correctly mirrors the changes in build.yml, ensuring the Jacoco coverage workflow has access to the same test resources.

balta/src/test/resources/db/postgres/11_testing.strange_columns_data.sql (1)

17-19: LGTM!

The INSERT statement correctly uses quoted identifiers matching the DDL and provides clean baseline test data.

balta/src/main/scala/za/co/absa/db/mag/core/SqlItem.scala (1)

19-21: LGTM!

Clean, minimal trait definition following the interface segregation principle. This abstraction enables polymorphic handling of SQL fragments across the new composition framework.

balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryUnitTests.scala (1)

23-34: LGTM!

The tests correctly verify SqlEntry's concatenation behavior (with space insertion) and toString implementation. The test expectations align with the SqlEntry.concat logic for handling spacing between SQL fragments.

balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (2)

95-95: LGTM!

The API change from addNull to add("text_field", NULL) provides a more uniform parameter handling interface, consistent with the new typed parameter framework.


176-238: The test code is correct and does not require changes. The Params.add() method automatically handles column name quoting through ColumnReference.apply(), which intelligently quotes identifiers when necessary:

  • Unquoted input like "Col1", "col 1", "col-1", "colá", "1col" are automatically quoted to match the table schema.
  • Quoted input like "\"Col1\"" is preserved as-is but produces identical SQL entries.
  • The ColumnReference unit tests confirm that both approaches yield equivalent behavior (equal sqlEntry values).

All test expectations are valid: initial count is 1 (from the data script), the two inserts add rows 2 and 3, the three where queries correctly filter by different column references, and the final cleanup verifies count returns to 0.

Likely an incorrect or invalid review comment.

balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala (2)

24-30: LGTM! Well-structured test fixtures.

The test fixtures are clearly defined and reusable across test cases. The setup covers common SQL composition elements (columns, table, condition, function, parameter).


32-96: Good test coverage for SQL composition DSL.

The tests comprehensively cover the main SQL composition scenarios including SELECT, INSERT, and DELETE with various clauses. Consider adding tests for edge cases such as empty conditions, special characters in column/table names, or NULL handling in future iterations.

balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1)

21-119: Comprehensive test coverage for ColumnReference API.

The tests thoroughly validate the ColumnReference factory behavior including quoting rules, equality semantics, and pattern matching. Good coverage of edge cases like special characters, numeric prefixes, and cross-type equality.

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (2)

41-51: Enhanced column label resolution for quoted identifiers.

The logic correctly handles quoted column labels by first attempting an exact match, then falling back to the inner (unquoted) label. This provides flexibility when working with quoted SQL identifiers.


152-152: Breaking change: Type alias renamed from FieldNames to ColumnNames.

This rename may break external code that references QueryResultRow.FieldNames. Ensure this is documented in the release notes and any dependent code is updated.

balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (2)

24-28: Clean migration to SqlEntry for type-safe SQL fragment handling.

The trait now returns SqlEntry instead of String for sqlEntry and equalityOperator, enabling type-safe composition with the new SQL DSL. The default implementations using cached SqlEntry instances (sqlQuestionMark, sqlEquals) are efficient.


47-51: Correct NullParamValue implementation.

The NullParamValue correctly provides NULL for sqlEntry and IS for equalityOperator, enabling proper SQL composition for null comparisons (e.g., column IS NULL).

balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)

27-30: Signature change is correct; pipeline failure requires investigation of actual SQL output.

The signature change from String to SqlEntry and List to Vector is correctly implemented. The SqlEntry.concat() logic properly handles spacing for SQL syntax (adds space except before ,, (, ), . or after (). The WHERE clause composition at DBTable.scala:243 should produce correctly formatted SQL like id_field = ?.

Without access to the actual pipeline failure logs or generated SQL, the root cause cannot be confirmed. The issue may originate elsewhere or require deeper investigation into specific test execution.

balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (2)

19-27: Custom equality implementation looks correct.

The equality based on sqlEntry comparison and corresponding hashCode implementation is appropriate for this domain type. However, note that SqlEntry is an AnyVal (value class), so sqlEntry.hashCode will delegate to String.hashCode on the underlying entry field, which is the expected behavior.


42-50: Factory method pattern matching is well-structured.

The cascading pattern match correctly handles:

  1. Standard SQL identifiers (lowercase with underscores)
  2. Quoted but regular names (removes unnecessary quotes)
  3. Already-quoted special names (preserves as-is)
  4. Unquoted special names (adds quoting with escaping)

This provides a safe abstraction for column name handling.

balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (1)

241-247: Well-structured WHERE condition builder.

The paramsToWhereCondition method correctly builds AND-joined conditions using the typed SqlEntry operators. The use of foldRight with accumulator and toSqlEntry(" AND ") is idiomatic.

balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (2)

41-49: SQL construction using the DSL is correct.

The function call SQL generation properly builds parameter entries from both ordered and named params, with named params using the := operator for PostgreSQL named parameter syntax. The functionName(paramsLine) correctly produces function_name(param1, param2, name := value).


144-154: Factory methods provide convenient construction.

The overloaded apply methods allow creating DBFunction instances with different parameter configurations cleanly. The use of SqlEntry(functionName) wraps the string appropriately.

balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (3)

19-36: LGTM!

The sealed abstract class design with a private constructor and abstract accessors provides a clean foundation for the typed parameter model. The immutable collection types are appropriate.


133-135: LGTM!

Clean factory method for creating empty NamedParams instances.


172-174: LGTM!

Clean factory method for creating empty OrderedParams instances.

* code improvements
@github-actions
Copy link

github-actions bot commented Dec 17, 2025

JaCoCo 'balta' module code coverage report - scala 2.12.18

Overall Project 74.5% -29.18% 🍏
Files changed 65.72% 🍏

File Coverage
DBQuerySupport.scala 100% 🍏
SqlEntryComposition.scala 96.95% -7.11% 🍏
ColumnReference.scala 96% -51.71% 🍏
QueryResultRow.scala 92.37% -3.67% 🍏
QueryResult.scala 88.66% 🍏
DBTable.scala 87.06% -9.7% 🍏
QueryParamValue.scala 74.47% 🍏
QueryResultRowImplicits.scala 74.27% -4.68% 🍏
Params.scala 68.75% -12.5% 🍏
SqlEntry.scala 67.9% -86.08% 🍏
DBFunction.scala 62.21% 🍏
Postgres.scala 15.22% -26.09% 🍏

Copy link

@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)
balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (1)

176-238: Good test coverage for edge cases with special column names.

The test exercises important edge cases including:

  • Mixed-case column names (col1 vs Col1)
  • Columns with spaces, hyphens, and unicode characters
  • Quoted vs unquoted column name handling

Minor: there's an indentation inconsistency on lines 200-234 (extra leading space).

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (3)

33-33: Custom == method shadows inherited equals and only compares to String.

The custom ==(other: String) method only handles String comparison and shadows Any.==. This can lead to confusing behavior:

  • sqlEntry == "foo" uses your method
  • sqlEntry == anotherSqlEntry uses reference equality from AnyVal

Consider renaming to something like matches or equalsString to avoid confusion, or implement proper equality for SqlEntry comparisons as well.

-  def ==(other: String): Boolean = this.entry == other
+  def matches(other: String): Boolean = this.entry == other

96-108: Variable shadowing reduces readability.

Lines 97-98 define val first and val second which shadow the method parameters of the same names. This makes the code harder to follow.

       case (Some(e1), Some(e2)) =>
-        val first = e1.entry
-        val second = e2.entry
+        val firstStr = e1.entry
+        val secondStr = e2.entry
         val e2Start = e2.entry(0) // first character of second.entry, can never be empty here because of the previous cases
         val e1End = e1.entry.last // last character of the first.entry, can never be empty here because of the previous cases
         val noSpaceStartChars = Set(',', '(', ')', '.')
         val noSpaceEndChars = Set('(')
         if (noSpaceStartChars.contains(e2Start) || noSpaceEndChars.contains(e1End)) {
           // It looks better to have so space before characters like , ( )
-          Some(SqlEntry(s"$first$second"))
+          Some(SqlEntry(s"$firstStr$secondStr"))
         } else {
-          Some(SqlEntry(s"$first $second"))
+          Some(SqlEntry(s"$firstStr $secondStr"))
         }

101-102: Consider extracting spacing character sets as companion object constants.

The noSpaceStartChars and noSpaceEndChars sets are created on every concat call. For a minor performance improvement and better maintainability, these could be extracted as private constants in the companion object.

 object SqlEntry {
+  private val noSpaceStartChars = Set(',', '(', ')', '.')
+  private val noSpaceEndChars = Set('(')
+
   def apply(entry: String): SqlEntry = new SqlEntry(entry)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (3)

37-39: Minor style inconsistency in object instantiation.

SelectFragment and InsertFragment use () while DeleteFragment omits them. While both work, consistency is preferred.

   private object SelectFragment extends SelectFragment()
   private object InsertFragment extends InsertFragment()
-  private object DeleteFragment extends DeleteFragment
+  private object DeleteFragment extends DeleteFragment()

108-108: Fix formatting: missing space before =.

-  def BY(columns: ColumnReference*): OrderByFragment= new OrderByFragment(columnsToSqlEntry(columns))
+  def BY(columns: ColumnReference*): OrderByFragment = new OrderByFragment(columnsToSqlEntry(columns))

121-128: Consider unifying column-to-entry conversion logic.

This logic is similar to lines 10-11 in SelectFragment.apply:

val allFields = firstField +: fields
new SelectWithFieldsFragment(select + allFields.map(_.sqlEntry).mkSqlEntry(", "))

Consider extracting a shared helper or using the existing SqlEntry.apply(ColumnReference*) factory from the companion object for consistency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e3f5c1 and fa3c6ed.

📒 Files selected for processing (7)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (7 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (11 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (3 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (5)
balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala (1)
  • this (41-43)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (3)
  • apply (143-145)
  • apply (147-149)
  • apply (151-153)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (2)
  • apply (9-12)
  • apply (14-14)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (4)
  • apply (42-50)
  • apply (52-54)
  • apply (70-70)
  • sqlEntry (59-59)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (4)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • QueryParamType (32-92)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (3)
  • QueryParamValue (30-53)
  • sqlEntry (26-26)
  • equalityOperator (27-27)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (8)
  • Params (29-36)
  • Params (37-175)
  • OrderedParams (141-170)
  • OrderedParams (172-174)
  • values (127-127)
  • values (169-169)
  • paramNames (130-130)
  • items (128-128)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (5)
  • SqlEntry (23-58)
  • SqlEntry (60-113)
  • toOption (52-54)
  • mkSqlEntry (82-84)
  • mkSqlEntry (85-88)
balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (4)
balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala (3)
  • table (88-90)
  • add (122-124)
  • add (144-146)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • NULL (86-86)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (2)
  • next (50-58)
  • noMore (63-63)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (4)
  • getLong (95-95)
  • getLong (96-96)
  • getString (89-89)
  • getString (90-90)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (2)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (2)
  • SqlEntry (23-58)
  • SqlEntry (60-113)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (1)
  • sql (41-48)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (2)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (11)
  • SqlEntry (23-58)
  • SqlEntry (60-113)
  • apply (35-38)
  • apply (47-50)
  • apply (61-61)
  • apply (62-65)
  • apply (67-67)
  • mkSqlEntry (82-84)
  • mkSqlEntry (85-88)
  • prefix (70-70)
  • toOption (52-54)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (4)
  • apply (42-50)
  • apply (52-54)
  • apply (70-70)
  • sqlEntry (59-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Scala 2.11.12
🔇 Additional comments (15)
balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (2)

68-70: LGTM!

Test assertion string updated consistently across both the return value and assertion.


95-95: LGTM!

Correctly uses the new typed parameter API with Params.add("text_field", NULL) for deleting rows with NULL values.

balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala (1)

24-98: Comprehensive test coverage for the SQL DSL.

The test suite validates the key SQL composition patterns including SELECT, INSERT, DELETE with various combinations of WHERE, ORDER BY, VALUES, and RETURNING clauses. The tests are well-organized and readable.

balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (3)

221-241: LGTM!

The private helper methods (composeSelectAndRun, composeDeleteAndRun, composeCountAndRun) are cleanly refactored to use the SqlEntry DSL. The composition pattern with optional WHERE and ORDER BY clauses is handled correctly.


243-249: LGTM!

The paramsToWhereCondition method correctly builds parameterized WHERE clauses by combining column names with their equality operators and values using AND. Using foldRight with prepending maintains parameter order.


41-50: The INSERT SQL generation correctly produces single parentheses around column and value lists.

The implementation is correct:

  1. columns is a Vector[SqlEntry] of column names
  2. table(columns) invokes apply(params: Seq[SqlEntry], foo: String = "")
  3. params.mkSqlEntry("(", ",", ")") produces (col1,col2) as a single SqlEntry
  4. The + operator concatenates table name with the parenthesized columns—the concat function avoids extra spacing before (, producing tableName(col1,col2)
  5. Final result: INSERT INTO tableName(col1, col2) VALUES(val1, val2) RETURNING *

Unit tests confirm the expected single-parentheses format. No double parentheses issue exists.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (1)

40-50: Dummy parameter is a workaround for type erasure - consider alternative approaches.

Using foo: String = "" as a dummy disambiguator works but is a code smell. The Scaladoc correctly explains the rationale, which is good.

Alternative approaches (for future consideration):

  • Use a different method name like applySeq
  • Use a marker type instead of String

Current implementation is acceptable given the documentation.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (3)

1-5: LGTM!

Package declaration and imports are appropriate for the DSL functionality being introduced.


41-63: LGTM!

The fragment classes and mix-in traits provide a clean, composable API for SQL clause construction. The use of traits for OrderByMixIn and ReturningMixIn enables flexible query composition.


65-97: LGTM!

The sealed query hierarchy correctly models SQL query state transitions. The design prevents invalid compositions (e.g., multiple WHERE clauses) through the type system.

balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (5)

19-24: LGTM!

The imports are appropriate for the new SqlEntry-based composition and typed parameter containers.


95-98: LGTM!

The conversion of the orderBy string to Option[SqlEntry] via .toOption correctly handles empty strings, and the parameter values are properly combined for query execution.


108-131: LGTM!

The immutable pattern is correctly maintained - setParam and clear return new instances rather than mutating state.


163-166: LGTM!

The sealed case class pattern is valid here - it provides value equality and copy methods while preventing external subclassing. The factory methods in the companion object correctly wrap string function names in SqlEntry.

Also applies to: 199-202


41-48: No action needed—ORDER BY implementation correctly handles None parameters.

The logic is sound. When orderBy is None, BY(None) creates an OrderByFragment with sqlEntry = None, and SqlEntry + Option[SqlEntry] correctly returns the original SQL unchanged without appending an ORDER BY clause.

@benedeki benedeki changed the title Feature/62 make composition of sql expression easier and safer #62: Make composition of SQL expression easier and safer Dec 17, 2025
Copy link

@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 (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1)

121-128: Consider using mkSqlEntry for consistency.

The manual string construction at lines 125-126 could use the mkSqlEntry helper method for consistency with the rest of the DSL.

Apply this diff to use mkSqlEntry:

 private def columnsToSqlEntry(fields: Seq[ColumnReference]): Option[SqlEntry] = {
   if (fields.isEmpty) {
     None
   } else {
-    val fieldEntries = fields.map(_.sqlEntry.entry)
-    Some(SqlEntry(fieldEntries.mkString(", ")))
+    Some(fields.map(_.sqlEntry).mkSqlEntry(", "))
   }
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa3c6ed and 9df6834.

📒 Files selected for processing (2)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (2)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (2)
  • SqlEntry (23-58)
  • SqlEntry (60-113)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (1)
  • sql (41-48)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (11)
  • SqlEntry (23-58)
  • SqlEntry (60-113)
  • apply (35-38)
  • apply (47-50)
  • apply (61-61)
  • apply (62-65)
  • apply (67-67)
  • mkSqlEntry (82-84)
  • mkSqlEntry (85-88)
  • prefix (70-70)
  • toOption (52-54)
🔇 Additional comments (16)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (3)

20-20: LGTM!

The import is necessary to support the SqlEntry-based API migration.


30-30: LGTM!

Correctly extracts the underlying SQL string from the SqlEntry wrapper using .entry.


27-27: Signature change from String to SqlEntry and List to Vector is appropriate and properly implemented.

Vector provides random access and update in effectively constant time, which is more suitable than List for indexed access patterns in query execution. All callers (DBFunction and DBTable) have been correctly updated to pass Vector[QueryParamValue] instances, confirming the API change is complete across the codebase.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (13)

1-5: LGTM!

Package declaration and imports are appropriate for the SQL composition DSL.


8-15: LGTM!

The SelectFragment design correctly enforces at least one field in the SELECT clause through the firstField parameter, preventing empty SELECT statements.


17-31: LGTM!

The InsertFragment and QueryInsertIntoFragment correctly enforce at least one value in the VALUES clause and provide convenient overloads.


33-35: LGTM!

The DeleteFragment provides a straightforward DELETE FROM builder.


37-39: LGTM!

The private singleton pattern is appropriate for the fragment builders.


41-43: LGTM!

The SelectWithFieldsFragment correctly chains FROM after SELECT fields.


45-47: LGTM!

The OrderByFragment correctly handles optional ORDER BY clauses using the prefix helper.


49-52: LGTM!

The OrderByMixIn trait provides reusable ORDER BY functionality through composition.


54-63: LGTM!

The ReturningMixIn trait correctly provides RETURNING clause support with type-safe field handling.


65-96: LGTM!

The sealed class hierarchy effectively models SQL query states and transitions, enforcing correct SQL structure through types. The WHERE clause handling correctly supports both mandatory and optional conditions.


98-101: LGTM!

The SqlEntryConstant design correctly encapsulates SQL constants with controlled instantiation.


103-108: LGTM!

The builder accessor methods provide a clean API for SQL composition with convenient overloads for ORDER BY.


113-119: LGTM!

Caching SQL keyword fragments is a good optimization and improves consistency.

…-names-are-not-case-insensitive' into feature/62-make-composition-of-sql-expression-easier-and-safer

# Conflicts:
#	balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala
#	balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala
Copy link

@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

♻️ Duplicate comments (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (1)

112-112: Good security practice: implicit String conversion is disabled.

The commented-out StringToSqlEntry implicit conversion with a TODO is the correct approach. This prevents developers from accidentally passing unsanitized user input as string literals, which would bypass the type-safe parameter handling.

Note: A similar implicit conversion exists in SqlEntry.scala (line 91). Consider aligning both files by either:

  1. Disabling it in both places, or
  2. Adding strong documentation warnings in SqlEntry.scala if keeping it there

Based on the past review comment on this file, the security implications of implicit String conversions have already been raised. The current state (commented out) reflects good judgment.

🧹 Nitpick comments (5)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (3)

33-33: Consider using equals instead of overriding ==.

Overriding the == operator in Scala can lead to confusion because it doesn't align with equals() and hashCode(). The standard pattern is to override equals and hashCode in the companion object for value classes.

However, since this is a value class extending AnyVal, custom equality is limited. The current implementation compares string content, which may be acceptable for this use case.

📋 Consider this alternative if referential equality is needed

If you need both string-based and referential equality:

def contentEquals(other: String): Boolean = this.entry == other

Then users can call sqlEntry.contentEquals(str) explicitly.


47-50: Document the workaround for type erasure.

The foo parameter is a clever workaround for JVM type erasure, but the current comment could be clearer. Consider renaming to something more descriptive like disambiguator or adding a link to the erasure issue.

📋 Suggested improvement
  /** Translates a sequence of SqlEntry entries into a single SqlEntry formatted as a parameter list
    *
    * @param params - A sequence of SqlEntry to be included as parameters.
-   * @param foo    - A bogus parameter do differentiate this method from the `apply(params: String*)` method, as their
-  *                  signatures would be same otherwise after type erasure.
+   * @param disambiguator - Unused parameter to differentiate this method from `apply(params: String*)` 
+   *                        after JVM type erasure (both would have signature `apply(Seq)`).
    * @return         A new SqlEntry that combines the input as a list of parameters/columns for a function or table.
    */
- def apply(params: Seq[SqlEntry], foo: String = ""): SqlEntry = {
+ def apply(params: Seq[SqlEntry], disambiguator: String = ""): SqlEntry = {

103-110: Consider edge cases in the spacing heuristic.

The spacing logic handles common cases well, but may have edge cases:

  • What about operators like =, <, >, etc.?
  • What about string literals or quoted identifiers that contain these characters?

Since SqlEntry is a simple string wrapper, it can't distinguish between SQL keywords and string literals. This is acceptable if SqlEntry is only used for structural SQL parts (not user data), but worth documenting.

Consider adding a comment explaining that SqlEntry should only contain SQL structure (keywords, operators, identifiers), not user data values (which should go through parameters).

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (1)

40-43: Suggest improving the error message for better user experience.

The error message currently shows the normalized form (columnNameQuoteless) rather than the original input. This could confuse users who provided a different casing or quoted name.

🔎 Proposed improvement
 def columnNumber(columnName: String): Int = {
   val columnNameQuoteless = ColumnName(columnName).quoteLess
-  columnNames.getOrThrow(columnNameQuoteless, new NoSuchElementException(s"Column '$columnNameQuoteless' not found"))
+  columnNames.getOrThrow(columnNameQuoteless, new NoSuchElementException(s"Column '$columnName' not found (normalized to '$columnNameQuoteless')"))
 }
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (1)

62-62: Consider clarifying the deprecation message.

The deprecation message references NULL, but users need to import QueryParamType.NULL to use it. Consider making this explicit in the message.

🔎 Suggested improvement
-@deprecated("Use add(paramName, NULL)", "balta 0.3.0")
+@deprecated("Use add(paramName, QueryParamType.NULL) or import QueryParamType.NULL", "balta 0.3.0")
-@deprecated("Use add(NULL)", "balta 0.3.0")
+@deprecated("Use add(QueryParamType.NULL) or import QueryParamType.NULL", "balta 0.3.0")

Also applies to: 86-86

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9df6834 and a9e9abf.

📒 Files selected for processing (14)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala
  • balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala
  • balta/src/main/scala/za/co/absa/db/balta/postgres/implicits/Postgres.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala
  • balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala
  • balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala
  • balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala
  • balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala
🚧 Files skipped from review as they are similar to previous changes (4)
  • balta/src/test/scala/za/co/absa/db/mag/core/SqlEntryCompositionUnitTests.scala
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala
🧰 Additional context used
🧬 Code graph analysis (8)
balta/src/main/scala/za/co/absa/db/balta/postgres/implicits/Postgres.scala (2)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (4)
  • columnNumber (40-43)
  • getArray (118-123)
  • getArray (125-125)
  • getArray (127-138)
balta/src/main/scala/za/co/absa/db/balta/postgres/classes/SimpleJsonString.scala (2)
  • SimpleJsonString (29-29)
  • SimpleJsonString (31-37)
balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (5)
  • QueryResultRow (35-139)
  • QueryResultRow (141-185)
  • columnNamesFromMetadata (153-155)
  • getOffsetDateTime (109-109)
  • getOffsetDateTime (110-110)
balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
  • QueryResultRow (35-139)
  • QueryResultRow (141-185)
  • columnNamesFromMetadata (153-155)
balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (6)
balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala (3)
  • table (88-90)
  • add (122-124)
  • add (144-146)
balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (7)
  • delete (163-165)
  • delete (167-169)
  • DBTable (31-250)
  • where (84-86)
  • where (97-99)
  • where (109-111)
  • where (122-124)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (6)
  • Params (29-36)
  • Params (37-175)
  • add (51-53)
  • add (75-77)
  • add (105-109)
  • add (149-153)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • NULL (86-86)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (2)
  • next (50-58)
  • noMore (63-63)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (4)
  • getLong (86-86)
  • getLong (87-87)
  • getString (80-80)
  • getString (81-81)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
  • QueryResultRow (35-139)
  • QueryResultRow (141-185)
  • columnNamesFromMetadata (153-155)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (4)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (6)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • apply (47-66)
  • apply (68-70)
  • apply (78-78)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (2)
  • apply (9-12)
  • apply (14-14)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (3)
  • apply (143-145)
  • apply (147-149)
  • apply (151-153)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (2)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (9)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • ColumnName (21-32)
  • ColumnName (77-79)
  • ColumnReference (34-75)
  • apply (47-66)
  • apply (68-70)
  • apply (78-78)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (16)
  • QueryParamType (32-92)
  • NULL (86-86)
  • toQueryParamValue (35-35)
  • toQueryParamValue (39-39)
  • toQueryParamValue (43-43)
  • toQueryParamValue (47-47)
  • toQueryParamValue (51-51)
  • toQueryParamValue (55-55)
  • toQueryParamValue (59-59)
  • toQueryParamValue (63-63)
  • toQueryParamValue (67-67)
  • toQueryParamValue (71-71)
  • toQueryParamValue (75-75)
  • toQueryParamValue (79-79)
  • toQueryParamValue (83-83)
  • toQueryParamValue (89-89)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (2)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (5)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • ColumnName (21-32)
  • ColumnName (77-79)
balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala (1)
  • getOrThrow (28-30)
🔇 Additional comments (20)
balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala (1)

75-89: LGTM! Consistent naming convention applied.

The rename from columnLabel to columnName aligns with the broader API changes throughout the codebase. The TODO comment on Line 82 appropriately flags the unused TypeTag context bound for future cleanup.

balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala (1)

32-46: LGTM! Test updated to reflect API rename.

The test correctly calls columnNamesFromMetadata instead of the deprecated fieldNamesFromMetadata. Test expectations remain unchanged, confirming behavioral consistency.

balta/src/main/scala/za/co/absa/db/balta/postgres/implicits/Postgres.scala (1)

28-32: LGTM! Public API parameter rename is consistent.

The parameter rename from columnLabel to columnName aligns with the updated QueryResultRow API and improves naming clarity.

balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala (2)

32-54: LGTM! Test updated with typo fix.

The test correctly uses columnNamesFromMetadata and fixes a typo in the variable name (expecedResultexpectedResult). Test behavior is preserved.


195-197: LGTM! Variable naming improved.

The rename from resultOfColumnLabel to resultOfColumnName is consistent with the API changes.

balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (3)

68-70: LGTM! Test data updated.

The return value change is a simple test data update with no functional impact.


95-95: LGTM! Improved NULL handling.

Using Params.add("text_field", NULL) is more type-safe and idiomatic than raw SQL string manipulation.


176-236: Excellent test coverage for special column names.

This test validates the new SqlEntry and ColumnReference handling for edge cases including:

  • Quoted identifiers ("Col1")
  • Mixed-case names (automatically lowercased)
  • Names with spaces, hyphens, special characters, and starting with digits

The test correctly exercises both parameter-based insertion and retrieval, ensuring the quoting logic works bidirectionally.

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (2)

19-19: LGTM! Type alias updated consistently.

The import change from FieldNames to ColumnNames reflects the updated public API from QueryResultRow.


34-34: LGTM! Initialization updated to use renamed method.

The call to columnNamesFromMetadata is consistent with the updated QueryResultRow API.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (2)

8-97: Well-structured fluent SQL DSL.

The composition API provides a type-safe way to construct SQL queries with:

  • Clear separation between query stages (SELECT...FROM, WHERE, ORDER BY, etc.)
  • Mix-in traits for reusable clauses (OrderByMixIn, ReturningMixIn)
  • Sealed class hierarchy preventing invalid query states

The design enforces correct SQL syntax at compile time, which is the primary goal stated in the PR objectives.


111-111: The implicit conversion is safe in practice given the sealed type hierarchy.

The implicit QueryToSqlEntry conversion cannot hide type errors because the Query type and all its subtypes are sealed, preventing unexpected implementations. Additionally, the codebase does not differentiate between specific Query subtypes—all methods accept the base Query class and inherit the same sqlEntry property. The conversion is a simple property delegation with no loss of type information.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (1)

91-91: The implicit conversion direction is reversed in this comment.

Line 91 contains implicit def sqlEntryToString(sqlEntry: SqlEntry): String, which converts SqlEntry to String — not String to SqlEntry as stated in the review. This conversion is safe and does not enable SQL injection: it only allows existing SqlEntry objects to be used where strings are expected, such as for logging or string concatenation.

The inverse conversion (StringToSqlEntry) that would pose the security concern you've identified is commented out at SqlEntryComposition.scala line 112 with a TODO, confirming the team is aware of the risk and has explicitly disabled it. No implicit conversion from raw strings to SqlEntry exists in the active codebase.

Likely an incorrect or invalid review comment.

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)

21-21: LGTM! Import aligns with the new column handling.

The import of ColumnName supports the typed column reference approach introduced in this PR.


33-37: LGTM! Improved naming clarity.

Renaming columnLabels to columnNames better reflects the intent and aligns with SQL terminology.


53-125: LGTM! Consistent parameter renaming across all accessor methods.

All getter methods have been consistently updated to use columnName instead of columnLabel, and properly delegate to the updated columnNumber method.

balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (4)

21-36: LGTM! Excellent sealed hierarchy design.

The sealed abstract class with a private constructor and well-defined abstract methods creates a type-safe, controlled API. The new imports support the typed parameter handling introduced in this PR.


39-40: LGTM! Type-safe parameter storage.

The use of ColumnName and ColumnReference as keys instead of raw strings provides compile-time safety and aligns with the PR's goal of safer SQL composition.


96-135: LGTM! Type-safe named parameter handling.

The use of ColumnName as a map key with proper equals/hashCode based on sqlEntry ensures that different representations of the same column name (e.g., "MyColumn" vs "mycolumn") are treated as equivalent. The new paramNames method is a useful addition for introspection.


141-174: LGTM! Ordered parameter handling is correct.

The implementation correctly generates 1-based ColumnIndex keys for SQL positioning while providing 0-based Vector access through apply(position). The separation between SQL parameter positions (1-based) and programmatic access (0-based) is appropriate.

Comment on lines +143 to 155
type ColumnNames = Map[String, Int]
type TransformerFnc[T] = Object => T
type Extractor = ResultSet => Option[Object]
type Extractors = Vector[Extractor]

def apply(resultSet: ResultSet)(implicit fieldNames: FieldNames, extractors: Extractors): QueryResultRow = {
def apply(resultSet: ResultSet)(implicit columnNames: ColumnNames, extractors: Extractors): QueryResultRow = {
val fields = extractors.map(_(resultSet))
new QueryResultRow(resultSet.getRow, fields, fieldNames)
new QueryResultRow(resultSet.getRow, fields, columnNames)
}

def fieldNamesFromMetadata(metaData: ResultSetMetaData): FieldNames = {
def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the QueryResultRow.scala file and examine the relevant code
fd -e scala -path "*QueryResultRow*"

Repository: AbsaOSS/balta

Length of output: 225


🏁 Script executed:

# Search for the ColumnName class and its quoteLess implementation
fd -e scala | xargs rg -l "ColumnName" | head -20

Repository: AbsaOSS/balta

Length of output: 492


🏁 Script executed:

# Look for test files related to QueryResultRow
fd -e scala -path "*test*" | xargs rg -l "QueryResultRow" 2>/dev/null

Repository: AbsaOSS/balta

Length of output: 1111


🏁 Script executed:

# Read the QueryResultRow.scala file
cat -n balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala

Repository: AbsaOSS/balta

Length of output: 9930


🏁 Script executed:

# Search for ColumnName class definition and quoteLess method
rg -n "class ColumnName|def quoteLess|object ColumnName" balta/src/main/scala/

Repository: AbsaOSS/balta

Length of output: 258


🏁 Script executed:

# Look for quoted column name tests
rg -n -i "quoted|special|\".*[A-Z]" balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala

Repository: AbsaOSS/balta

Length of output: 4021


🏁 Script executed:

# Read the ColumnName class to understand quoteLess implementation
cat -n balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala | head -100

Repository: AbsaOSS/balta

Length of output: 3855


🏁 Script executed:

# Search for any column name transformation or normalization
rg -n "quoteLess|getColumnName" balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala

Repository: AbsaOSS/balta

Length of output: 221


🏁 Script executed:

# Check how columnNamesFromMetadata is used in tests or other code
rg -n "columnNamesFromMetadata" balta/src/

Repository: AbsaOSS/balta

Length of output: 968


🏁 Script executed:

# Check if there are any tests with mixed-case or quoted column names
rg -n "test.*[A-Z]|\".*[A-Z]" balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala | head -30

Repository: AbsaOSS/balta

Length of output: 1478


🏁 Script executed:

# Look for the full integration test file
cat -n balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala | head -60

Repository: AbsaOSS/balta

Length of output: 2303


🏁 Script executed:

# Search for any other uses of columnNamesFromMetadata or related setup
rg -B5 -A5 "columnNamesFromMetadata" balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala

Repository: AbsaOSS/balta

Length of output: 497


🏁 Script executed:

# Look for all column names in all tests to see if any are mixed case or quoted
rg -n "test.*[A-Z_]|\"[A-Z]" balta/src/test/scala/za/co/absa/db/balta/

Repository: AbsaOSS/balta

Length of output: 14142


🏁 Script executed:

# Search for table definitions or schema creation to understand what column names exist
fd -e sql -o -e scala | xargs rg -l "CREATE TABLE|column" | head -5

Repository: AbsaOSS/balta

Length of output: 521


🏁 Script executed:

# Check if there are any schema or migration files
fd -e sql balta/src/test/

Repository: AbsaOSS/balta

Length of output: 422


🏁 Script executed:

# Look at the test with strange column names
sed -n '176,230p' balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala

Repository: AbsaOSS/balta

Length of output: 2112


🏁 Script executed:

# Look at the schema for the strange columns table
cat balta/src/test/resources/db/postgres/10_testing.strange_columns.sql

Repository: AbsaOSS/balta

Length of output: 1132


🏁 Script executed:

# Look at the strange columns data insertion
cat balta/src/test/resources/db/postgres/11_testing.strange_columns_data.sql

Repository: AbsaOSS/balta

Length of output: 823


🏁 Script executed:

# Check if there's a mismatch - examine how ColumnName.quoteLess handles unquoted vs quoted names
python3 << 'EOF'
# Simulate the ColumnName behavior based on the patterns I found

def simulate_column_name_quoteless(name):
    """Simulate ColumnName.quoteLess behavior"""
    trimmed = name.strip()
    
    # Regular lowercase pattern: ^([a-z_][a-z0-9_]*)$
    if all(c.islower() or c.isdigit() or c == '_' for c in trimmed) and trimmed and (trimmed[0].isalpha() or trimmed[0] == '_'):
        return trimmed
    
    # Mixed case pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
    if all(c.isalpha() or c.isdigit() or c == '_' for c in trimmed) and trimmed and (trimmed[0].isalpha() or trimmed[0] == '_'):
        return trimmed.lower()
    
    # Quoted regular pattern: ^\"([a-z_][a-z0-9_]*)\"$
    if trimmed.startswith('"') and trimmed.endswith('"'):
        unquoted = trimmed[1:-1]
        if all(c.islower() or c.isdigit() or c == '_' for c in unquoted) and unquoted and (unquoted[0].isalpha() or unquoted[0] == '_'):
            return unquoted
        # Quoted pattern with any content: return unescaped
        return unquoted.replace('""', '"')
    
    # Other: quote and escape
    return trimmed

# Test cases from the strange_columns table
test_cases = [
    ("col1", "col1 (unquoted lowercase)"),
    ("Col1", "Col1 (unquoted mixed case)"),
    ('"Col1"', '"Col1" (quoted mixed case)'),
    ('"col 1"', '"col 1" (quoted with space)'),
    ('"col-1"', '"col-1" (quoted with dash)'),
    ('"colá"', '"colá" (quoted with accent)'),
    ('"1col"', '"1col" (quoted, starts with digit)'),
]

print("ColumnName.quoteLess simulation:")
for name, desc in test_cases:
    result = simulate_column_name_quoteless(name)
    print(f"  {desc:40} → {result}")

# PostgreSQL JDBC metadata behavior
print("\nPostgreSQL JDBC getColumnName() behavior:")
pg_metadata_returns = {
    "col1": "col1",           # unquoted → lowercase
    '"Col1"': "Col1",         # quoted → unquoted, case-preserved
    '"col 1"': "col 1",       # quoted → unquoted, space preserved
    '"col-1"': "col-1",       # quoted → unquoted, dash preserved
    '"colá"': "colá",         # quoted → unquoted, accent preserved
    '"1col"': "1col",         # quoted → unquoted, leading digit
}

for original, pg_returns in pg_metadata_returns.items():
    print(f"  {original:15} → JDBC returns: {pg_returns}")
    quoteless = simulate_column_name_quoteless(original)
    match = "✓ MATCH" if quoteless == pg_returns else "✗ MISMATCH"
    print(f"                    ColumnName.quoteLess: {quoteless:15} {match}")

EOF

Repository: AbsaOSS/balta

Length of output: 143


🏁 Script executed:

# Verify the actual behavior by checking how the test passes - look at what metadata keys are created
rg -B10 -A10 "columnNamesFromMetadata" balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala

Repository: AbsaOSS/balta

Length of output: 789


🏁 Script executed:

# Check if there are any tests or usage that shows accessing mixed-case unquoted columns
rg -n "getString\(\"[A-Z]" balta/src/test/scala/za/co/absa/db/balta/

Repository: AbsaOSS/balta

Length of output: 39


🏁 Script executed:

# Verify the actual JDBC getColumnName behavior by checking any database-specific documentation or comments
rg -n "getColumnName|metadata|JDBC|ResultSetMetaData" balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala

Repository: AbsaOSS/balta

Length of output: 376


🏁 Script executed:

# Search for any handling of quoted identifiers in the codebase
rg -n "quoteLess|getColumnName" balta/src/main/scala/za/co/absa/db/balta/classes/ -B2 -A2

Repository: AbsaOSS/balta

Length of output: 1245


Apply ColumnName.quoteLess consistently in columnNamesFromMetadata.

The columnNamesFromMetadata method stores raw JDBC metadata column names as map keys, while the columnNumber lookup method applies ColumnName.quoteLess transformation to user input. This mismatch works for PostgreSQL only because the transformation happens to align with JDBC's metadata output (unquoted identifiers as lowercase, quoted identifiers unquoted with case preserved). However, this relationship is implicit and not enforced by the type system. To make the code more robust and database-agnostic, apply ColumnName.quoteLess to all column names when building the map, ensuring consistency between storage and lookup logic.

🤖 Prompt for AI Agents
In balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala around
lines 143 to 155, columnNamesFromMetadata currently uses raw JDBC column names
as map keys while lookups use ColumnName.quoteLess, causing inconsistent
behavior across databases; update columnNamesFromMetadata to apply
ColumnName.quoteLess to each metaData.getColumnName(i) when building the Map so
stored keys match the lookup normalization (i.e., transform the metadata name
with ColumnName.quoteLess before using it as the Map key).

Copy link

@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

♻️ Duplicate comments (1)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (1)

40-43: Apply ColumnName.quoteLess consistently in columnNamesFromMetadata.

The columnNumber method applies ColumnName.quoteLess to the lookup key (line 41), but columnNamesFromMetadata (line 154) stores raw JDBC metadata column names. This inconsistency works for PostgreSQL due to alignment between JDBC's behavior and the quoteLess transformation, but the relationship is implicit and database-dependent. Applying ColumnName.quoteLess when building the map in columnNamesFromMetadata would ensure robust, database-agnostic behavior.

🔎 Recommended fix
  def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
-    Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap
+    Range.inclusive(1, metaData.getColumnCount).map(i => ColumnName(metaData.getColumnName(i)).quoteLess -> i).toMap
  }
🧹 Nitpick comments (1)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (1)

33-33: Consider clarifying the custom == operator behavior.

The == method compares only the entry field, which works for direct comparisons but doesn't integrate with Scala's equality contract. This won't affect usage in collections or pattern matching since value classes use structural equality by default. However, the custom == method may create confusion about which equality semantics apply in different contexts.

If the intent is purely string comparison, consider naming this method differently (e.g., matches, equalsString) to avoid confusion with standard equality:

🔎 Alternative naming suggestion
-  def ==(other: String): Boolean = this.entry == other
+  def matches(other: String): Boolean = this.entry == other
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9e9abf and 3095caf.

📒 Files selected for processing (4)
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala
  • balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala
🧰 Additional context used
🧬 Code graph analysis (3)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (4)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
  • apply (50-50)
  • apply (56-56)
  • apply (148-151)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (2)
  • apply (9-12)
  • apply (14-14)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (3)
  • apply (143-145)
  • apply (147-149)
  • apply (151-153)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (26-26)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (8)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • ColumnName (21-32)
  • ColumnName (77-79)
  • apply (47-66)
  • apply (68-70)
  • apply (78-78)
balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala (1)
  • getOrThrow (28-30)
balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala (1)
  • getOrThrow (27-29)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (1)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (16)
  • QueryParamType (32-92)
  • NULL (86-86)
  • toQueryParamValue (35-35)
  • toQueryParamValue (39-39)
  • toQueryParamValue (43-43)
  • toQueryParamValue (47-47)
  • toQueryParamValue (51-51)
  • toQueryParamValue (55-55)
  • toQueryParamValue (59-59)
  • toQueryParamValue (63-63)
  • toQueryParamValue (67-67)
  • toQueryParamValue (71-71)
  • toQueryParamValue (75-75)
  • toQueryParamValue (79-79)
  • toQueryParamValue (83-83)
  • toQueryParamValue (89-89)
🔇 Additional comments (10)
balta/src/main/scala/za/co/absa/db/mag/core/SqlEntry.scala (2)

47-50: Disambiguator parameter serves type-erasure workaround.

The disambiguator parameter exists solely to differentiate this overload from apply(params: String*) after JVM type erasure. While documented, this pattern can confuse callers who might pass values for this parameter.

The documentation clearly explains the purpose, and the default empty string makes it transparent to callers.


91-91: Implicit String conversion is convenient but use with awareness.

The implicit conversion from SqlEntry to String provides convenient interoperability but may mask type safety in some scenarios. Since SqlEntry is a value class wrapping a string, the conversion is straightforward and the performance impact is minimal.

This design choice aligns well with the fluent API goals of the DSL.

balta/src/main/scala/za/co/absa/db/mag/core/SqlEntryComposition.scala (3)

24-27: String-based VALUES method requires care with user input.

The VALUES(firstValue: String, otherValues: String*) method wraps strings directly in SqlEntry. While convenient for literals, this could enable SQL injection if user input is passed directly. The safer VALUES(values: Seq[SqlEntry]) overload at lines 28-31 should be preferred when composing values from external sources.

Ensure coding guidelines emphasize using parameterized queries or the Seq[SqlEntry] overload for dynamic values.


111-111: Implicit Query to SqlEntry conversion is safe and ergonomic.

The implicit conversion extracts the sqlEntry field from a Query, enabling seamless use of query builders in contexts expecting SqlEntry. This is safe since it doesn't construct SQL from arbitrary input.


8-97: Well-structured DSL with type-safe query building.

The sealed class hierarchy and stage-based builders (SelectFragment → SelectWithFieldsFragment → QuerySelect) provide compile-time safety, preventing invalid SQL construction paths. The use of mixins for ORDER BY and RETURNING clauses enables clean reuse across query types.

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (1)

21-21: API renaming improves clarity and aligns with SQL terminology.

The transition from FieldNames/columnLabel to ColumnNames/columnName better reflects standard SQL terminology and improves code readability. The import of ColumnName utility at line 21 supports the new typed column handling.

Also applies to: 33-37

balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (4)

29-40: Refactored parameter model provides unified position-based access.

The abstract Params class now exposes a consistent interface with position-based access (apply(position: Int)), while concrete implementations (NamedParams, OrderedParams) manage their internal storage differently. The type aliases NamedParamsMap and ParamsList improve code clarity.


96-131: NamedParams now uses typed ColumnName keys for safer parameter handling.

The migration from string keys to ColumnName keys (lines 106, 123) provides stronger typing and consistent column name normalization. The paramNames accessor (line 130) exposes the typed keys, enabling better introspection.


141-174: OrderedParams provides position-indexed parameter storage.

The OrderedParams implementation uses ColumnReference(size + 1) to create positional references (line 150), maintaining a clean vector-based storage model. The companion object factory (lines 172-174) simplifies construction.


62-62: Deprecation notices guide migration to QueryParamType.NULL.

The addNull methods are properly deprecated with clear migration guidance pointing to QueryParamType.NULL. This maintains backward compatibility while encouraging the new pattern.

Also applies to: 86-86, 118-118, 162-162

…d-safer' of https://github.com/AbsaOSS/balta into feature/62-make-composition-of-sql-expression-easier-and-safer
@benedeki benedeki changed the base branch from master to bugfix/1-field-parameter-names-are-not-case-insensitive December 30, 2025 15:54

test("Composition of `INSERT INTO ...(...) VALUES ... RETURNING ...` using field names") {
val fields = SqlEntry(field1, field2)
val query = INSERT INTO tableName(fields.entry) VALUES("100, 'Sample Text'") RETURNING(ALL)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wonderful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependent The item depends on some other open item (Issue or PR)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make composition of SQL expression easier and safer

3 participants