Skip to content

Conversation

@benedeki
Copy link
Contributor

@benedeki benedeki commented Dec 10, 2025

  • introduced classes ColumnReference and ColumnName to represent column and parameter names and handle them properly in SQL manner
  • Reworked Params class hierarchy to work with the new classes and for better structure
  • Introduced SqlItem trait shared between all classes providing SQLEntry
  • DBTable, DBFunction and DBQuerySupport now utilize the streamlined Params structure

Overview

Release Notes

  • Addressed column names case sensitivity (Balta handle column names as case sensitive, if they are mixed case or containing non-standard characters Balta envelopes the column name with quotes, per SQL standard, forcing the DB to take the name literally.)

Related

Closes #1
Closes #2

Summary by CodeRabbit

  • New Features

    • Improved support for unusual column names (spaces, special chars, Unicode) and enhanced column-reference handling.
    • Clearer separation of positional vs named parameters for queries.
  • Tests

    • Added unit/integration tests and SQL fixtures exercising strange column identifiers.
    • CI database setup now loads the new test table and data during pipeline setup.
  • Refactor

    • Public APIs renamed to use "columnName" for clarity; parameter-related APIs adjusted.

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

* introduced classes `ColumnReference` and `ColumnName` to represent column and parameter names and handle them properly in SQL manner
* Reworked `Params` class hierarchy to work with the new classes and for better structure
* Introduced `SqlItem` trait shared between all classes providing `SQLEntry`
* `DBTable`, `DBFunction` and `DBQuerySupport` now utilize the streamlined `Params` structure
@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

Walkthrough

Separates params into ordered and named containers, adds ColumnReference/SqlItem for normalized/quoted column identifiers, migrates internal param collections from List to Vector, renames field/metadata APIs to use "columnName", adds tests and SQL fixtures for strange column names, and loads those fixtures in CI DB setup.

Changes

Cohort / File(s) Summary
Workflows
​.github/workflows/build.yml, ​.github/workflows/jacoco_report.yml
CI "Setup database" step now runs 10_testing.strange_columns.sql and 11_testing.strange_columns_data.sql after existing DB setup scripts.
Params core
balta/src/main/scala/.../inner/Params.scala
Rework Params into abstract base with OrderedParams (positional) and NamedParams (named); storage uses Vector/ParamsList and a NamedParamsMap; new APIs: size/values/items/apply and factory constructors; deprecated some addNull helpers.
DBFunction & query plumbing
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala, balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala, balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala
DBFunction now holds orderedParams + namedParams, new apply overloads, updated setParam/clear; runQuery and internal helpers switched from List[QueryParamValue]Vector[QueryParamValue]; SQL composition updated to use OrderedParams/NamedParams items and ColumnReference.
Column reference & SQL item
balta/src/main/scala/za/co/absa/db/mag/core/SqlItem.scala, balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala
New SqlItem trait and ColumnReference/ColumnName/ColumnIndex types added with quoting, escaping, normalization, sqlEntry rendering, apply/unapply and equality semantics.
QueryResult / row API renames
balta/src/main/scala/.../classes/QueryResultRow.scala, balta/src/main/scala/.../classes/QueryResult.scala, balta/src/main/scala/.../implicits/QueryResultRowImplicits.scala, balta/src/main/scala/.../postgres/implicits/Postgres.scala
Renamed FieldNamesColumnNames and fieldNamesFromMetadatacolumnNamesFromMetadata; replaced columnLabel parameter names with columnName; lookups now use ColumnName/quote-less keys.
Tests — unit & integration
balta/src/test/scala/.../ColumnReferenceUnitTests.scala, balta/src/test/scala/.../DBTableIntegrationTests.scala, balta/src/test/scala/.../QueryResultRowIntegrationTests.scala, balta/src/test/scala/.../PostgresRowIntegrationTests.scala
Added ColumnReference unit tests; updated integration tests to exercise strange/quoted column names and to use new Params APIs and renamed metadata helpers; minor test renames/adjustments.
Test SQL fixtures
balta/src/test/resources/db/postgres/10_testing.strange_columns.sql, balta/src/test/resources/db/postgres/11_testing.strange_columns_data.sql
New SQL fixtures create a table with diverse/quoted column identifiers and insert a sample row used by tests and CI DB setup.

Sequence Diagram(s)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

breaking change

Suggested reviewers

  • lsulak
  • salamonpavel
  • miroslavpojer

Poem

🐇 I split my carrots—ordered and named,

Quoted strange columns so none go unnamed.
Vectors and tests hopped into place,
Lookups now gentle, no more frantic chase.
A tiny rabbit cheers this tidy refactor!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Field/parameter names are not case insensitive' clearly and specifically describes the main problem being addressed in the changeset, which is handling column/parameter names properly with respect to SQL identifier semantics.
Description check ✅ Passed The pull request description includes all required sections: a comprehensive overview of changes (ColumnReference/ColumnName classes, Params rework, SqlItem trait), release notes explaining the column name case sensitivity fix, and linked issue references (#1 and #2).
Linked Issues check ✅ Passed The pull request meets the requirements of both linked issues: introduces ColumnReference/ColumnName classes for proper SQL identifier handling [#1, #2], implements quoting for mixed-case/non-standard characters [#2], and ensures case-insensitive semantics for unquoted identifiers [#1].
Out of Scope Changes check ✅ Passed All changes are directly related to the objectives of handling column/parameter names with proper SQL semantics. The workflow updates, parameter refactoring, test additions, and API updates all support the core goals of addressing case sensitivity and implementing escaping.
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 bugfix/1-field-parameter-names-are-not-case-insensitive

📜 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 7d81fb3 and 2e09d2f.

📒 Files selected for processing (2)
  • .github/workflows/build.yml
  • .github/workflows/jacoco_report.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/jacoco_report.yml
🔇 Additional comments (1)
.github/workflows/build.yml (1)

74-75: The SQL fixture files for this workflow step are in place.

The addition of these SQL setup scripts aligns with the PR objectives for testing case-insensitive and special character handling in column names. The execution order (DDL before data) and sequential numbering are correct.


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 self-assigned this Dec 10, 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: 4

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

53-53: Consider returning Option[String] from unapply for idiomatic pattern matching.

The unapply method returns String directly instead of Option[String]. While this works in Scala 3 and some Scala 2 contexts, the conventional pattern for extractors is:

def unapply(columnName: ColumnName): Option[String] = Some(columnName.enteredName)

This ensures compatibility with all pattern matching contexts.

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

31-35: Consider adding scaladoc to abstract methods.

The abstract methods lack documentation. While semantics are intuitive, documenting expected behavior (e.g., whether apply(position) is 0-based) would improve API clarity.


167-169: Consider documenting that apply(position) uses 0-based indexing.

The apply(position: Int) method uses 0-based vector indexing, while the internally stored ColumnIndex is 1-based (for SQL). This is correct but potentially confusing for API users expecting SQL-style 1-based positions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (14)
  • .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 (8 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 (5 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/mag/core/ColumnReference.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/DBTestSuiteUnitTests.scala (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)
🧰 Additional context used
🧬 Code graph analysis (9)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (1)
  • sql (39-47)
balta/src/main/scala/za/co/absa/db/mag/core/SqlItem.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1)
  • sqlEntry (56-56)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (5)
  • apply (123-123)
  • apply (125-125)
  • apply (134-134)
  • apply (167-167)
  • apply (173-173)
balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (7)
  • ColumnReference (33-68)
  • sqlEntry (56-56)
  • hashCode (30-30)
  • unapply (53-53)
  • unapply (72-72)
  • ColumnName (21-31)
  • ColumnName (70-73)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
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/mag/core/ColumnReference.scala (3)
  • apply (38-47)
  • apply (49-51)
  • apply (71-71)
balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (3)
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/balta/typeclasses/QueryParamValue.scala (2)
  • sqlEntry (24-24)
  • equalityOperator (25-25)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)
  • runQuery (26-47)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (3)
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/classes/inner/Params.scala (18)
  • Params (29-36)
  • Params (37-175)
  • NamedParams (96-131)
  • NamedParams (133-135)
  • OrderedParams (141-170)
  • OrderedParams (172-174)
  • values (127-127)
  • values (169-169)
  • items (128-128)
  • add (51-53)
  • add (75-77)
  • add (105-109)
  • add (149-153)
  • apply (123-123)
  • apply (125-125)
  • apply (134-134)
  • apply (167-167)
  • apply (173-173)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamValue.scala (1)
  • sqlEntry (24-24)
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 (6)
  • ColumnName (21-31)
  • ColumnName (70-73)
  • ColumnReference (33-68)
  • apply (38-47)
  • apply (49-51)
  • apply (71-71)
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/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (2)
balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (7)
  • delete (159-161)
  • delete (163-165)
  • DBTable (29-257)
  • where (80-82)
  • where (93-95)
  • where (105-107)
  • where (118-120)
balta/src/main/scala/za/co/absa/db/balta/typeclasses/QueryParamType.scala (1)
  • NULL (86-86)
🔇 Additional comments (24)
.github/workflows/build.yml (1)

74-75: LGTM!

The additional database setup steps correctly load the schema and test data for the new strange column name tests, following the established pattern.

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

17-19: LGTM!

The INSERT statement correctly populates test data with unconventional column identifiers (mixed case, spaces, special characters, Unicode, starting with digit), which aligns with the PR objective to test case-insensitive handling and quoting of column names.

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

95-95: LGTM!

The update to Params.add("text_field", NULL) correctly uses the new explicit-value API, replacing the deprecated addNull method.


176-238: Excellent test coverage for unconventional column identifiers.

This test comprehensively validates the PR objectives by exercising:

  • Inserts with both unquoted and explicitly quoted column names
  • Query filtering across various special identifier formats (mixed case, spaces, hyphens, accented characters, digit-prefixed)
  • Proper cleanup

The test ensures that the framework correctly handles SQL-standard identifier quoting and case sensitivity rules.

.github/workflows/jacoco_report.yml (1)

51-52: LGTM!

Consistent with the build workflow, these additions ensure the code coverage job has access to the strange column test data.

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

19-21: LGTM!

Clean trait providing a lightweight contract for types that produce SQL fragment representations. This supports the new column reference system introduced in the PR.

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

17-33: LGTM!

The table schema provides comprehensive coverage of SQL identifier edge cases, including mixed case, spaces, special characters, Unicode, and digit-prefixed names. This properly supports testing the PR's objectives around case-insensitive handling and automatic quoting of non-standard identifiers.

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

26-26: All runQuery call sites properly updated to Vector.

Verified that all callers pass Vector[QueryParamValue] to runQuery:

  • DBFunction.scala:96 uses orderedParams.values ++ namedParams.values (both return Vector)
  • DBTable.scala:46, 223, 231, 238 all receive Vector[QueryParamValue] parameters

The parameter type change from List[QueryParamValue] to Vector[QueryParamValue] is complete across all implementations.

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

39-46: LGTM! Clean pattern matching for INSERT column handling.

The implementation correctly differentiates between NamedParams (which generates explicit column names) and OrderedParams (which relies on positional VALUES only). Using columnName.sqlEntry ensures proper quoting for column names with special characters.


251-256: LGTM! WHERE clause now uses SQL-safe column names.

The use of columnName.sqlEntry ensures column names with special characters or mixed case are properly quoted in the generated WHERE clause.


217-224: LGTM! Vector is appropriate for parameter collections.

The change from List to Vector aligns with DBQuerySupport.runQuery signature and provides better performance characteristics for the indexed iteration patterns used during parameter binding.

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

152-164: LGTM! Clean rename from FieldNames to ColumnNames.

The type alias and method signature updates are consistent with the broader naming convention changes in the PR.


42-51: Reject case-insensitive normalization; column names should remain case-sensitive per PR design.

The PR objective explicitly states "Field/parameter names are NOT case insensitive." The current implementation correctly preserves column name case as returned by the database metadata. Normalizing unquoted labels to lowercase would contradict this design decision and break databases that return uppercase or mixed-case column names (e.g., Oracle). The quoted vs. unquoted handling in the current code (lines 43-50) properly respects SQL semantics where quoted identifiers retain their case while unquoted identifiers follow database-specific folding rules.

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

21-31: LGTM! Equality based on sqlEntry correctly implements case-insensitive semantics.

The equals and hashCode implementations ensure that different representations of the same SQL identifier (e.g., ab and "ab") are treated as equal, which aligns with the PR objective of SQL-standard case insensitivity.

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

39-47: LGTM! SQL generation correctly separates positional and named parameters.

The implementation properly:

  1. Generates positional parameter placeholders from orderedParams.values
  2. Generates named parameter assignments using columnName.sqlEntry := value.sqlEntry
  3. Concatenates them in the correct order (positional first, then named)

94-97: LGTM! Parameter binding order matches SQL placeholder order.

The combined values orderedParams.values ++ namedParams.values correctly align with the SQL placeholder order generated in sql().


146-152: LGTM! Convenient factory methods for common parameter patterns.

The overloads cleanly support both named-only and positional-only parameter initialization scenarios.

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

68-72: Verify that sqlEntry properly wraps quoted identifiers.

The SQL standard requires quoted identifiers to be wrapped in delimiters with internal quotes doubled. An identifier containing a quote should produce "a""b" (in SQL), which in Scala string form would be "\"a\"\"b\"", not "a\"\"b" as currently expected by the test.

If the implementation produces unquoted output like a""b, the resulting SQL would be malformed—interpreted as multiple tokens rather than a single identifier. Either the implementation or test expectations need correction to properly quote the identifier.

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

105-109: LGTM — immutable update pattern is correct.

The approach of creating a new NamedParams instance with updated map is clean and thread-safe. The ListMap preserves insertion order as needed.


123-123: Lookup relies on ColumnName equality — verify consistency.

The lookup creates a new ColumnName(paramName) and uses it as a map key. This will only work correctly if ColumnName equality is case-insensitive for standard identifiers. If add("Param1", v) stores a ColumnNameQuoted but apply("param1") looks up with ColumnNameSimple, the lookup will fail with NoSuchElementException.

Consider providing a safer accessor that returns Option[QueryParamValue] or validates the key exists.


149-153: LGTM — parameter indexing is consistent.

The ColumnIndex uses 1-based indexing (SQL convention) while vector storage uses 0-based indexing. This is correct for SQL parameter binding.


62-65: Good deprecation practice with clear migration path.

The deprecation annotation includes version info and guidance to use add(paramName, QueryParamType.NULL) instead. Clean approach for phasing out the dedicated null methods.


133-135: LGTM — companion object factories.

Clean factory methods enabling NamedParams() and OrderedParams() construction without exposing the new keyword externally.

Also applies to: 172-174


39-40: Verify case-insensitive lookup behavior with ColumnName keys in code review.

The NamedParamsMap uses ColumnName as keys, which could potentially cause case-sensitivity issues. Ensure that the ColumnName type and ColumnReference.apply() method properly handle case normalization. Specifically, verify that ColumnName("param1") and ColumnName("PARAM1") resolve to equivalent keys when representing unquoted SQL identifiers, particularly if Issue #1 establishes a requirement for SQL case-insensitivity alignment.

@github-actions
Copy link

github-actions bot commented Dec 10, 2025

JaCoCo 'balta' module code coverage report - scala 2.12.18

Overall Project 71.84% -12.58% 🍏
Files changed 69.03% 🍏

File Coverage
DBQuerySupport.scala 100% 🍏
ColumnReference.scala 96.72% -51.34% 🍏
QueryResultRow.scala 92.37% -3.67% 🍏
DBTable.scala 88.78% -3.06% 🍏
QueryResult.scala 88.66% 🍏
QueryResultRowImplicits.scala 74.27% -4.68% 🍏
Params.scala 68.75% -12.5% 🍏
DBFunction.scala 61.06% -41.15% 🍏
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: 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 d6aee9d and b73ff73.

📒 Files selected for processing (3)
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (2 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala
🧰 Additional context used
🧬 Code graph analysis (2)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (3)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (5)
  • apply (123-123)
  • apply (125-125)
  • apply (134-134)
  • apply (167-167)
  • apply (173-173)
balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala (3)
  • apply (142-144)
  • apply (146-148)
  • apply (150-152)
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/classes/QueryResult.scala (1)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala (3)
  • QueryResultRow (36-148)
  • QueryResultRow (150-194)
  • fieldNamesFromMetadata (162-164)
⏰ 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). (2)
  • GitHub Check: Scala 2.11.12
  • GitHub Check: Scala 2.12.18
🔇 Additional comments (5)
balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala (1)

19-35: ColumnNames import and implicit type alignment look correct

The new ColumnNames import and the updated implicit fieldNames: ColumnNames are consistent with QueryResultRow.fieldNamesFromMetadata and keep behavior unchanged. No functional or API concerns here from this refactor.

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

38-39: Helper methods are correctly implemented.

The quote and escapeQuote functions properly handle SQL identifier quoting: internal quotes are doubled, then the result is wrapped in double quotes (e.g., a"b"a""b").


57-65: Case classes correctly model the domain.

The three case classes properly represent simple column names, exact (quoted) names, and index-based references with appropriate sqlEntry generation.


51-56: LGTM: Companion object methods follow Scala conventions.

The factory methods and extractors provide clean APIs for constructing and pattern-matching column references.

Also applies to: 68-71


24-30: equals and hashCode implementation is correct.

Comparing sqlEntry values provides the right semantics for case-insensitive equality once the factory method properly normalizes unquoted identifiers (see comment on lines 34-35).

Comment on lines 34 to 35
private val regularColumnNamePattern = "^([a-z_][a-z0-9_]*)$".r
private val quotedRegularColumnNamePattern = "^\"([a-z_][a-z0-9_]*)\"$".r
Copy link

@coderabbitai coderabbitai bot Dec 10, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Regex patterns break case-insensitive identifier handling.

Two issues prevent proper case-insensitive comparison:

  1. Line 34: regularColumnNamePattern only matches lowercase identifiers. Uppercase unquoted identifiers like ABC fall through to line 48 and get quoted as "ABC", making them unequal to abc. Per SQL semantics and the PR objective (Issue Field/parameter names are not case insensitive #1), unquoted identifiers should be case-insensitive.

  2. Line 35: Missing * quantifier causes quotedRegularColumnNamePattern to only match 2-character identifiers.

Apply this diff:

-  private val regularColumnNamePattern = "^([a-z_][a-z0-9_]*)$".r
-  private val quotedRegularColumnNamePattern = "^\"([a-z_][a-z0-9_]*)\"$".r
+  private val regularColumnNamePattern = "^([a-zA-Z_][a-zA-Z0-9_]*)$".r
+  private val quotedRegularColumnNamePattern = "^\"([a-z_][a-z0-9_]*)\"$".r

Then normalize in the factory method (line 44):

-      case regularColumnNamePattern(columnName) => ColumnNameSimple(columnName)
+      case regularColumnNamePattern(columnName) => ColumnNameSimple(columnName.toLowerCase)

This ensures abc, ABC, and AbC all map to the same sqlEntry value for case-insensitive equality.

🤖 Prompt for AI Agents
In balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala around
lines 34-35, the two regexes are incorrect: change regularColumnNamePattern to
accept letters in both cases (e.g. use [A-Za-z_] for the first char and
[A-Za-z0-9_]* for the rest) and fix quotedRegularColumnNamePattern to use the *
quantifier so it matches identifiers of any length; then in the factory method
around line 44, normalize matched identifier names to a consistent case (e.g.
toLowerCase) when creating the sqlEntry so unquoted identifiers like abc, ABC
and AbC produce the same sqlEntry for case-insensitive equality.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unquoted mixed case identifiers are auto-quoted.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the ColumnReferenceUnitTests to see what's tested for mixed-case identifiers
cat balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala

Length of output: 4574


@benedeki, I understand the current implementation auto-quotes mixed-case identifiers, but this approach contradicts the PR objective from Issue #1 and SQL standard semantics:

SQL Standard Behavior:

  • Unquoted identifiers are case-insensitive: abc, ABC, and AbC all refer to the same column
  • Quoted identifiers are case-sensitive: "abc""ABC"

Current Implementation:

  • abcabc (unquoted)
  • ABC"ABC" (quoted, now case-sensitive)
  • AbC"AbC" (quoted, now case-sensitive)

This means abcABCAbC, violating Issue #1's objective: "Ensure parameter/field name handling is case-insensitive to align framework behavior with SQL semantics."

Example Problem:

val params = NamedParams().add("UserId", 123)
// Creates "UserId" (quoted, case-sensitive)
// Won't match a column named userid or USERID

The test on Line 100 (assert(n1 != n2, "Expected inequality for different sqlEntry")) encodes this behavior but contradicts Issue #1.

Question: Is the intent to preserve mixed-case identifiers as case-sensitive (current implementation), or to make all unquoted identifiers case-insensitive per SQL standard (Issue #1 objective)? If the latter, unquoted identifiers should be normalized (e.g., lowercased) regardless of how they're entered.

…ain other non-standard characters

* unified naming - columnName(s) is used all around
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: 4

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b73ff73 and fd6e147.

📒 Files selected for processing (11)
  • balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala (6 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 (6 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/implicits/QueryResultRowImplicits.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/balta/postgres/implicits/Postgres.scala (1 hunks)
  • balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala (2 hunks)
  • balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala (3 hunks)
  • balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala (1 hunks)
  • balta/src/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • balta/src/test/scala/za/co/absa/db/balta/classes/DBTableIntegrationTests.scala
  • balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala
🧰 Additional context used
🧬 Code graph analysis (7)
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/main/scala/za/co/absa/db/balta/classes/DBTable.scala (2)
balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala (12)
  • Params (29-36)
  • Params (37-175)
  • OrderedParams (141-170)
  • OrderedParams (172-174)
  • values (127-127)
  • values (169-169)
  • paramNames (130-130)
  • add (51-53)
  • add (75-77)
  • add (105-109)
  • add (149-153)
  • items (128-128)
balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala (1)
  • runQuery (26-47)
balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala (6)
balta/src/main/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequired.scala (1)
  • stringPerConvention (33-36)
balta/src/main/scala/za/co/absa/db/mag/naming/implementations/MapBasedNaming.scala (1)
  • stringPerConvention (35-37)
balta/src/main/scala/za/co/absa/db/mag/naming/implementations/AsIsNaming.scala (1)
  • stringPerConvention (34-36)
balta/src/main/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNaming.scala (1)
  • stringPerConvention (49-51)
balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala (1)
  • value (37-37)
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/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-80)
  • 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/test/scala/za/co/absa/db/mag/core/ColumnReferenceUnitTests.scala (1)
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala (8)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • ColumnReference (34-75)
  • hashCode (31-31)
  • ColumnName (21-32)
  • ColumnName (77-80)
  • unapply (79-79)
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 (8)
  • core (40-40)
  • core (41-41)
  • core (42-45)
  • ColumnName (21-32)
  • ColumnName (77-80)
  • 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)
🪛 GitHub Actions: Build
balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala

[error] 21-21: method unapply is defined twice; conflicting symbols both originated in this file


[error] 22-22: Compilation failed: error near 'case class ColumnName private(enteredName: String,'

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

28-32: LGTM!

The name-based overloads follow a clean delegation pattern, routing through row.columnNumber(columnName) which now properly handles case-insensitive column lookups via the new ColumnName infrastructure.

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

75-89: LGTM!

The refactor consistently updates terminology from columnLabel to columnName throughout. The TODO comment properly tracks the TypeTag cleanup task via issue #64. The error message at line 87 correctly reflects the new column name terminology.

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

32-33: LGTM!

Test correctly updated to reflect the API rename from fieldNamesFromMetadata to columnNamesFromMetadata, also fixing the typo in the test name ("Metada" → "Metadata").

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

32-35: LGTM!

Tests correctly updated to reflect the API rename:

  • Method call updated to columnNamesFromMetadata
  • Typo fixed: expecedResultexpectedResult
  • Variable renamed: resultOfColumnLabelresultOfColumnName for consistency

Also applies to: 54-54, 195-197

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

21-166: Comprehensive test coverage for ColumnReference API.

The test suite thoroughly validates the new ColumnReference functionality including:

  • Regular and quoted name handling
  • Case-insensitivity via sqlEntry-based equality
  • Non-standard characters (spaces, dashes, non-ASCII, quotes)
  • Quote escaping and unescaping
  • Index-based column references

This aligns well with the PR objectives to handle column names per SQL standard.

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

38-46: LGTM!

The pattern matching on Params type correctly handles both NamedParams (generates column list from paramNames) and OrderedParams (empty column list for positional insertion). The use of _.sqlEntry ensures proper SQL quoting for non-standard column names.


217-241: LGTM!

The parameter type change from List[QueryParamValue] to Vector[QueryParamValue] aligns with the broader refactor across DBQuerySupport.runQuery and the Params hierarchy.


251-256: LGTM!

The refactored paramsToWhereCondition correctly uses columnName.sqlEntry to generate SQL-safe column references, ensuring proper quoting for columns with non-standard characters. This directly addresses PR objective #2 (escaping column/parameter names).

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

21-23: LGTM!

The new imports and type aliases cleanly integrate ColumnName and ColumnReference from the mag.core package. The ParamsList type alias provides a clear contract for the items representation.

Also applies to: 39-40


62-65: Deprecation annotations are well-structured.

Good use of @deprecated with clear migration guidance ("Use add(paramName, NULL)") and version info ("balta 0.3.0").

Also applies to: 86-89, 118-121, 162-165

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

40-43: LGTM on the column lookup logic.

Using ColumnName(columnName).quoteLess correctly normalizes user input for case-insensitive lookups, aligning with the PR objective from Issue #1.


143-143: Rename from FieldNames to ColumnNames is appropriate.

The new name better reflects SQL terminology and aligns with the ColumnName/ColumnReference abstractions introduced in this PR.

def apply(paramName: String): QueryParamValue = namedParams(ColumnName(paramName))

override def keys: Option[List[String]] = Some(items.keys.toList)
override def apply(position: Int): QueryParamValue = values(position)
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

Critical: Same off-by-one error in NamedParams.apply(position: Int).

If position is 1-based (consistent with SQL conventions and OrderedParams which uses 1-based ColumnReference indices), then values(position) should be values(position - 1).

🔎 Proposed fix
-    override def apply(position: Int): QueryParamValue = values(position)
+    override def apply(position: Int): QueryParamValue = values(position - 1)

Alternatively, clarify in the Scaladoc whether position is 0-based or 1-based to ensure consistent usage across the codebase.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override def apply(position: Int): QueryParamValue = values(position)
override def apply(position: Int): QueryParamValue = values(position - 1)
🤖 Prompt for AI Agents
In balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala around
line 125, NamedParams.apply currently uses values(position) which causes a 1-off
error when callers pass SQL-style 1-based positions; change the implementation
to index values(position - 1) and add a guard to validate position is between 1
and values.length (throw IndexOutOfBoundsException or similar) and update the
Scaladoc for NamedParams.apply to state that position is 1-based so callers and
OrderedParams remain consistent.

Comment on lines +167 to +169
override def apply(position: Int): QueryParamValue = items(position)._2
override def size: Int = items.size
override def values: Vector[QueryParamValue] = items.map(_._2)
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

Critical: Off-by-one error in apply(position: Int).

ColumnReference indices start at 1 (line 150: ColumnReference(size + 1)), but items is a 0-indexed Vector. Calling apply(1) accesses items(1) (second element) instead of items(0) (first element), causing IndexOutOfBoundsException for the first parameter.

🔎 Proposed fix
-    override def apply(position: Int): QueryParamValue = items(position)._2
+    override def apply(position: Int): QueryParamValue = items(position - 1)._2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override def apply(position: Int): QueryParamValue = items(position)._2
override def size: Int = items.size
override def values: Vector[QueryParamValue] = items.map(_._2)
override def apply(position: Int): QueryParamValue = items(position - 1)._2
override def size: Int = items.size
override def values: Vector[QueryParamValue] = items.map(_._2)
🤖 Prompt for AI Agents
In balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala around
lines 167 to 169, apply(position: Int) currently uses 0-based indexing against
the 1-based ColumnReference position, causing off-by-one and potential
IndexOutOfBounds; change the implementation to use items(position - 1)._2 and
add a bounds check (e.g., require(position >= 1 && position <= size, s"Invalid
parameter position: $position")) so callers using 1-based positions get the
correct element and invalid positions produce a clear error.

Comment on lines +153 to 155
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 | 🟠 Major

Major: Column name lookup will fail due to inconsistent normalization.

columnNumber (lines 40-42) normalizes the input via ColumnName(columnName).quoteLess (e.g., "MyColumn""mycolumn"), but columnNamesFromMetadata stores raw metadata names as keys without normalization.

This causes lookups to fail for mixed-case columns:

  • Metadata returns "MyColumn" → stored as key "MyColumn"
  • User queries "MyColumn" → lookup key is "mycolumn"NoSuchElementException
🔎 Proposed fix: Normalize keys when building the map
   def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
-    Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap
+    Range.inclusive(1, metaData.getColumnCount).map { i =>
+      val columnName = ColumnName(metaData.getColumnName(i))
+      columnName.quoteLess -> i
+    }.toMap
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap
}
def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
Range.inclusive(1, metaData.getColumnCount).map { i =>
val columnName = ColumnName(metaData.getColumnName(i))
columnName.quoteLess -> i
}.toMap
}

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.

Add escaping to column/parameter names Field/parameter names are not case insensitive

2 participants