Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow PredicateBuilder to recognize schema namespaced table names #48171

Merged
merged 1 commit into from May 11, 2023

Conversation

sholden
Copy link
Contributor

@sholden sholden commented May 8, 2023

Motivation / Background

ActiveRecord::PredicateBuilder currently assumes that a column name will only be specified in dot notation with a single period. If a column is specified in dot notation for a table that is namespaced in a schema, it will use the schema name as the table name. This PR allows columns to specified in the format schema.table.column as well as table.column.

This fixes #48172

Detail

This Pull Request changes the behavior of ActiveRecord::PredicateBuilder.references and ActiveRecord::PredicateBuilder#convert_dot_notation_to_hash

I didn't add this to the CHANGELOG, but if a reviewer thinks it's significant enough I'm happy to do so.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@sholden sholden force-pushed the predicate-builder-schemas branch 3 times, most recently from 904e308 to 0fbcb51 Compare May 8, 2023 22:55
@@ -30,7 +30,7 @@ def self.references(attributes)
if value.is_a?(Hash)
result << Arel.sql(key)
elsif key.include?(".")
result << Arel.sql(key.split(".").first)
result << Arel.sql(key.split(".")[0..-2].join("."))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think [0..-2] is a bit hard to grasp. Given we do this in two places it might make sense to extract a small private method here, e.g.

def split_column_name(key)
  *table, column = key.split(".")
  [table.join("."), column]
end

Copy link
Member

Choose a reason for hiding this comment

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

At a glance I feel key.split(".")[0..-2].join(".") is a bit expensive. How about String#[] like idx = key.rindex("."); key[0, idx]?

Copy link
Contributor Author

@sholden sholden May 9, 2023

Choose a reason for hiding this comment

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

I was a little meh on the split as well. I like the rindex idea, can rewrite that way. I just kept it since it was originally written that way. We could extract a method, but in one usage we need to return both the table and column, but in the other the column is ignored, so it might be wasteful to make an array and another string that we'd throw away.

@sholden sholden force-pushed the predicate-builder-schemas branch from 4a8899e to 1a06b1e Compare May 9, 2023 18:10
Copy link
Contributor

@casperisfine casperisfine left a comment

Choose a reason for hiding this comment

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

Please squash your commits.

Comment on lines 152 to 159
dot_notation = attributes.each_with_object({}) do |(k, v), dn|
next if v.is_a?(Hash)
idx = k.rindex(".")
dn[k] = idx if idx
end

dot_notation.each_key do |key|
table_name, column_name = key.split(".")
dot_notation.each do |key, idx|
table_name, column_name = key[0, idx], key[idx + 1, key.length]
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we merge these two loops and avoid the temporary Hash?

Copy link
Contributor Author

@sholden sholden May 10, 2023

Choose a reason for hiding this comment

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

We can't mutate the original attributes hash while iterating over it. But maybe we could save a hash allocation on L163 with #merge!? I'm always hesitant to mutate things getting passed in if I don't know all of the contexts where it would get used, but if we're already mutating attributes that are passed to #build_from_hash maybe that's already fine?

Copy link
Contributor

Choose a reason for hiding this comment

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

if we're already mutating attributes that are passed to #build_from_hash maybe that's already fine?

Yeah that was my point. But also I don't think we need to mutate attributes at all, just iterate once over it and create the returned hash as we go.

Copy link
Contributor Author

@sholden sholden May 10, 2023

Choose a reason for hiding this comment

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

Ah I gotcha. Updated to build a converted hash in one pass.

@sholden sholden force-pushed the predicate-builder-schemas branch 2 times, most recently from 781c082 to dcc429f Compare May 10, 2023 06:34
if !value.is_a?(Hash) && (idx = key.rindex("."))
table_name, column_name = key[0, idx], key[idx + 1, key.length]
converted[table_name] ||= {}
converted[table_name] = converted[table_name].merge(column_name => value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why you don't do:

Suggested change
converted[table_name] = converted[table_name].merge(column_name => value)
converted[table_name][column_name => value]

? That would avoid lots of Hash allocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Beyond the hash allocations, this single pass rewrite is breaking other tests. It's past midnight, going to revert and try again later. The way I'm attempting a single pass is causing keys to be overwritten. For example this is failing: https://github.com/sholden/rails/blob/predicate-builder-schemas/activerecord/test/cases/finder_test.rb#L1120

@casperisfine
Copy link
Contributor

One comment, other than that LGTM.

@sholden sholden force-pushed the predicate-builder-schemas branch 2 times, most recently from 04ab481 to 8b8d15b Compare May 10, 2023 19:29
@sholden
Copy link
Contributor Author

sholden commented May 10, 2023

Ok, I think this latest version is optimal as far as creating the converted hash in one pass with as few hash allocations as possible, albeit a little less readable. If it looks good I'll squash and clean up. I also added a test to ensure that we don't accidentally mutate the arguments originally passed to #where. In my first pass I realized updating with converted[table_name][column_name] = value as you suggested above was sometimes mutating the original hash, depending on the key ordering. I added the #dup call to prevent this from happening. Now, we will always merge in place or set keys on a fresh hash to avoid that scenario.

@casperisfine
Copy link
Contributor

Yeah, looks good now 👍. Thanks a lot.

@sholden sholden force-pushed the predicate-builder-schemas branch from 9c82468 to c5a99e0 Compare May 11, 2023 00:47
@sholden
Copy link
Contributor Author

sholden commented May 11, 2023

No problem. Think we're good to go then!

@byroot byroot merged commit 403e4e5 into rails:main May 11, 2023
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ActiveRecord::PredicateBuilder assumes all dot notation as "table.column"
4 participants