Skip to content

[fix](be) Limit parse_url QUERY key lookup to the query component - #68011

Open
jacktengg wants to merge 1 commit into
apache:masterfrom
jacktengg:DORIS-28860
Open

jacktengg wants to merge 1 commit into
apache:masterfrom
jacktengg:DORIS-28860

Conversation

@jacktengg

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Problem Summary:
parse_url(url, 'QUERY', key) searched for the key in the whole (trimmed) url instead of only in the query component, so text belonging to the path or to the fragment was accepted as a query key.

Reproduction:

SELECT parse_url('http://h/p#f?k=v', 'QUERY', 'k');  -- returns 'v' (expected NULL)
SELECT parse_url('http://h/p&k=v?x=1', 'QUERY', 'k'); -- returns 'v?x=1' (expected NULL)

Root cause:
UrlParser::parse_url_key scanned trimmed_url from the beginning and treated any ?/& preceded match as a query key, so it never checked whether the match was inside the real query component. Two further defects existed in the same loop: a key at offset 0 of the search window (the first key of the query, or the first key after a &) was rejected, and a key value was not bounded by the fragment, so # was reported as part of the value.

Fix:
Locate the real query component first - it starts at the first ? and ends at the # that starts the fragment - and reject urls whose # comes before the ?. The key/value scan is now bounded by that component, recognizes the first key and advances past each candidate so that no text is visited twice.

After the fix both statements above return NULL, and the first query parameter as well as duplicated keys (?k=1&k=2 - the last one wins, as before) are handled correctly.

Release note

Fix parse_url(url, 'QUERY', key) returning values from the path or the fragment for urls that do not contain the requested key in the query.

Check List (For Author)

  • Test: Regression test / Unit Test
    • New regression suite regression-test/suites/function_p0/test_parse_url_key.groovy (output generated with run-regression-test.sh --run -d function_p0 -s test_parse_url_key -forceGenOut, then re-run and passed).
    • Extended be/test/exprs/function/function_url_test.cpp with ParseUrlQueryKeyTest; FunctionUrlTEST.*, function_string_test.function_parse_url_test and function_string_test.function_extract_url_parameter_test all pass.
  • Behavior changed: Yes (see above, path/fragment text is no longer a query key)
  • Does this need documentation: No

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

### What problem does this PR solve?

Problem Summary:
`parse_url(url, 'QUERY', key)` searched for the key in the whole (trimmed) url instead of
only in the query component, so text belonging to the path or to the fragment was accepted
as a query key.

Reproduction:
```sql
SELECT parse_url('http://h/p#f?k=v', 'QUERY', 'k');  -- returns 'v' (expected NULL)
SELECT parse_url('http://h/p&k=v?x=1', 'QUERY', 'k'); -- returns 'v?x=1' (expected NULL)
```

Root cause:
`UrlParser::parse_url_key` scanned `trimmed_url` from the beginning and treated any `?`/`&`
preceded match as a query key, so it never checked whether the match was inside the real
query component. Two further defects existed in the same loop: a key at offset 0 of the
search window (the first key of the query, or the first key after a `&`) was rejected, and a
key value was not bounded by the fragment, so `#` was reported as part of the value.

Fix:
Locate the real query component first - it starts at the first `?` and ends at the `#` that
starts the fragment - and reject urls whose `#` comes before the `?`. The key/value scan is
now bounded by that component, recognizes the first key and advances past each candidate so
that no text is visited twice.

After the fix both statements above return NULL, and the first query parameter as well as
duplicated keys (`?k=1&k=2` - the last one wins, as before) are handled correctly.

### Release note

Fix `parse_url(url, 'QUERY', key)` returning values from the path or the fragment for urls
that do not contain the requested key in the query.

### Check List (For Author)

- Test: Regression test / Unit Test
    - New regression suite `regression-test/suites/function_p0/test_parse_url_key.groovy`
      (output generated with `run-regression-test.sh --run -d function_p0 -s test_parse_url_key -forceGenOut`,
      then re-run and passed).
    - Extended `be/test/exprs/function/function_url_test.cpp` with `ParseUrlQueryKeyTest`;
      `FunctionUrlTEST.*`, `function_string_test.function_parse_url_test` and
      `function_string_test.function_extract_url_parameter_test` all pass.
- Behavior changed: Yes (see above, path/fragment text is no longer a query key)
- Does this need documentation: No
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@jacktengg

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes for four inline findings: two result-correctness/compatibility issues, one mandatory regression-test lifecycle violation, and one avoidable per-row linear scan.

Critical checkpoint conclusions

  • Goal and proof: The keyed parser now confines lookup to the query component for the added path/fragment examples, but it is not correct as submitted: duplicate parameters regress from established first-wins behavior, and the two-argument parse_url(..., 'QUERY') path still disagrees with the keyed form.
  • Scope, clarity, and reuse: The change is localized and its conditions are readable, but query-component discovery is duplicated instead of shared across both BE arities and Nereids folding; this causes the overload inconsistency and redundant delimiter scan reported inline.
  • Concurrency: Calls use stack-local state and immutable same-TU searchers. No mutable shared state, locks, lock ordering, atomics, blocking work, or thread-context change is involved.
  • Lifecycle/static initialization: No new ownership or shutdown lifecycle exists. The existing static constants/searchers are initialized in dependency order in one translation unit, with no new cross-TU dependency or cycle.
  • Configuration: No configuration or dynamic-reload behavior is added.
  • Compatibility: No symbol, RPC, thrift, storage/serialization, or rolling-upgrade format changes apply. SQL result compatibility does apply: ?k=1&k=2 changes from 1 to 2 even though the PR describes duplicate behavior as unchanged.
  • Parallel paths: The BE two-argument overload and Nereids StringArithmetic.parseUrlQuery still treat ? after # as a query; both need matching fixes/tests. extract_url_parameter has analogous ordering and should be explicitly adjudicated.
  • Conditional logic: Apart from the accepted findings, the new start/&, =, component-order, empty-value, and progress rules were replayed across invalid-then-valid and repeated candidates; no additional missed match, nontermination, or bounds defect was substantiated.
  • Test coverage and results: Test discovery and all eight existing const/vector dispatch shapes were verified statically. Both regression queries are ordered, and every new .out row is correct for the keyed cases. The duplicate-key unit oracle is wrong, the parallel two-argument/FE cases are missing, and the suite's terminal table drop violates the mandatory preserve-for-debugging rule.
  • Observability: This deterministic local scalar parser needs no new log, metric, or tracing field.
  • Transactions, persistence, and writes: EditLog, failover, visible versions, MoW, production transactions, atomic writes, and crash recovery are not involved; regression DDL/DML is test setup only.
  • FE-BE variables/protocol: No variable or payload is added. FE/BE semantic parity matters only for the existing Nereids two-argument folding path noted above.
  • Performance, memory, errors, and nullability: One redundant full-suffix # scan is reported inline. Otherwise there is no significant new allocation, MemTracker obligation, ignored Status, exception-boundary issue, nullable-column misuse, COW issue, or lifetime defect.
  • Other core invariants: Data visibility, version consistency, delete bitmaps, storage correctness, and security boundaries are untouched. Scalar result correctness and per-row CPU work are fully represented by the inline findings.
  • User focus: No additional user-provided focus was supplied; the whole PR was reviewed.

This was a static-only review because the runner contract prohibits builds and tests. The author's reported unit/regression runs were not independently executed. Three review rounds completed; every final-round reviewer returned NO_NEW_VALUABLE_FINDINGS, all candidates were adjudicated, and the review converged cleanly at head 3140b5a5252dcaa61d2212a94c4f52667ce80c28.

// Remove leading and trailing spaces.
StringRef trimmed_url = url.trim();

// Search for the key in the url, ignoring malformed URLs for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Keep both parse_url forms consistent about query bounds

For the new reproducer http://h/p#f?k=v, this branch makes the three-argument form return NULL, but the unchanged UrlParser::parse_url(..., QUERY, ...) arm still finds the ? after # and returns k=v for the two-argument form. The patch therefore makes the same SQL function disagree about whether this URL has a query component, and the new regression suite cannot catch it because it calls only the keyed overload. Please centralize the ?/# bounds for both BE overloads and update Nereids StringArithmetic.parseUrlQuery plus literal/column test oracles as well; otherwise a BE-only correction will leave constant folding inconsistent. extract_url_parameter should also be explicitly checked for the analogous ordering.

if (query_pos < 0) {
// Query component is missing, the whole url is the path plus the fragment.
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Reuse the fragment offset instead of rescanning the query

Line 237 has already searched the whole trimmed URL for the first #, so after rejecting the # < ? case that absolute offset (or its absence) completely determines the query end. This second search walks the query suffix again on every row; for a long URL with no fragment, the same suffix is scanned twice for # before key lookup, contrary to the PR's claim that no text is visited twice. Please construct the query view directly from query_pos and fragment_pos (ideally in the shared bounds helper needed by the overload fix).

// Ending position is end of string.
value_end_pos = cast_set<int32_t>(query.size);
} else {
value_end_pos = value_pos + cast_set<int32_t>(value_end_rel_pos);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Preserve first-wins behavior for duplicate keys

The removed implementation returned immediately after extracting the first valid match, so parse_url('http://h/p?k=1&k=2#f', 'QUERY', 'k') returned 1. This loop now continues and overwrites result with 2, even though the PR and test comment say duplicate behavior is unchanged. Apache Hive's keyed parse_url also returns its first regex match. Please stop after the first valid query-component match and update the new duplicate-key oracle to expect 1; otherwise this focused fix introduces an unrelated SQL-result compatibility regression.

select id, parse_url(url, 'QUERY', 'x') as query_x from test_parse_url_key order by id
"""

sql """

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Preserve the table after the regression assertions

The repository's regression-test contract requires dropping tables before use, not after the test, so the populated state remains available for failure investigation. This suite already performs the required pre-test drop at lines 20-22; please remove this terminal drop block.

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.

2 participants