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

Support function table names in Arel Table #46864

Merged
merged 3 commits into from
Apr 25, 2023

Conversation

palkan
Copy link
Contributor

@palkan palkan commented Dec 31, 2022

Motivation / Background

Make it possible to select from computed/function table using Arel, or pass arbitrary string as a table name

This makes it possible to use Arel to query against functions, e.g., generate_series in PostgreSQL, or json_each, or whatever.

Detail

My particular use-case was in transforming the following SQL to Arel (for SQLite 3):

# posts:
#   title string
#   tags json (array)
Post.where(
  "EXISTS (SELECT 1 FROM json_each(posts.tags) WHERE value = ?)",
   tag
)

Currently, it's not possible to use computed table sources with Arel::Table due to the @name = name.to_s call.
The workaround is:

arel_table = Post.arel_table
table_name = Arel.sql(Arel::Nodes::NamedFunction.new("json_each", [arel_table[:tags]]).to_sql)
tags_arel = Arel::Table.new("", as: :json_tags)
# We can only overwrite the name value after the table is created
tags_arel.name = table_name

Post.where(tags_arel.project(1).where(tags_arel[:value].in(tags)).exists)

With this patch, we can provide a SqlLiteral (or a custom node) right to the Table constructor:

table_name = Arel.sql("json_each(table.tags)")
tags_arel = Arel::Table.new(table_name, as: :json_tags)


table_name = Arel::Nodes::NamedFunction.new("json_each", [arel_table[:tags]]).to_sql
tags_arel = Arel::Table.new(table_name, as: :json_tags)

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.

activerecord/lib/arel/table.rb Outdated Show resolved Hide resolved
activerecord/lib/arel/table.rb Outdated Show resolved Hide resolved
@palkan palkan force-pushed the feat/arel-function-tables branch 2 times, most recently from ffd4072 to a37945f Compare January 3, 2023 21:29
@name = name.to_s
@name =
case name
when Symbol then name.to_s
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To preserve current behavior (covered with tests). Not sure it leaked outside of tests though, but prefer to be defensive here.

@name =
case name
when Symbol then name.to_s
when Nodes::Node then Arel.sql(name.to_sql)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to not carry an ast node around and convert it into a SQL literal right here.

Alternatively, I considered updated the SQL visitor (to_sql.rb) and handle nodes in the #quote_table_name method. But that would affect some other nodes, too, so I hesitated.

Copy link
Contributor

Choose a reason for hiding this comment

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

The downside of calling .to_sql and wrapping it in a Arel::Nodes::SqlLiteral is that this looses track of potential bind params inside the expression. This might be undesirable, especially because the examples you bring up are function calls with parameters.

By not being able to use bind params, the resulting query will get a different signature for every set of params and can't be as easily reused as a prepared statement.

That being said, I agree that #quote_table_name probably isn't the best place to implement this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the resulting query will get a different signature for every set of params and can't be as easily reused as a prepared statement.

That's a good point (though in real life I prefer to avoid prepared statements).

Refactored to perform complication within a visitor (to_sql.rb#visit_Arel_Table).

Make it possible to select from computed/function table using Arel, or pass arbitrary string as a table name

This makes it possible to use Arel to query against functions, e.g., generate_series in PostgreSQL, or json_each, or whatever
@palkan
Copy link
Contributor Author

palkan commented Feb 24, 2023

Hey @matthewd @benedikt! Could you please take another look?

@benedikt
Copy link
Contributor

@palkan Looks good to me!

@palkan
Copy link
Contributor Author

palkan commented Apr 11, 2023

Looking for some feedback for this tiny but useful change /cc @rafaelfranca @matthewd

@rafaelfranca rafaelfranca merged commit 3a1662f into rails:main Apr 25, 2023
@palkan palkan deleted the feat/arel-function-tables branch April 25, 2023 18:33
alpaca-tc added a commit to alpaca-tc/activerecord-multi-tenant that referenced this pull request May 10, 2023
The `table_name` method was defined in `Arel::Nodes::Table` and `Arel::Nodes::TableAlias` to get the table name, but `Arel::Nodes::Table#table_name` was removed  rails/rails#46864.
Therefore, it is no longer possible to simply call `#table_name` to get `table_name`, so a `TableNode.table_name` has been added to get table_name from node.
alpaca-tc added a commit to alpaca-tc/activerecord-multi-tenant that referenced this pull request Sep 13, 2023
The `table_name` method was defined in `Arel::Nodes::Table` and `Arel::Nodes::TableAlias` to get the table name, but `Arel::Nodes::Table#table_name` was removed  rails/rails#46864.
Therefore, it is no longer possible to simply call `#table_name` to get `table_name`, so a `TableNode.table_name` has been added to get table_name from node.
alpaca-tc added a commit to alpaca-tc/activerecord-multi-tenant that referenced this pull request Sep 13, 2023
The `table_name` method was defined in `Arel::Nodes::Table` and `Arel::Nodes::TableAlias` to get the table name, but `Arel::Nodes::Table#table_name` was removed  rails/rails#46864.
Therefore, it is no longer possible to simply call `#table_name` to get `table_name`, so a `TableNode.table_name` has been added to get table_name from node.
gurkanindibay pushed a commit to citusdata/activerecord-multi-tenant that referenced this pull request Sep 14, 2023
* Add rails 7.1.0.beta1

* Support Rails7.1

The `table_name` method was defined in `Arel::Nodes::Table` and `Arel::Nodes::TableAlias` to get the table name, but `Arel::Nodes::Table#table_name` was removed  rails/rails#46864.
Therefore, it is no longer possible to simply call `#table_name` to get `table_name`, so a `TableNode.table_name` has been added to get table_name from node.
davinlagerroos pushed a commit to umn-asr/oracle-enhanced that referenced this pull request Feb 21, 2024
The table_name method was removed in rails/rails#46864
It was an alias of `name` which is still supported, so use `name` instead.
tagliala added a commit to ifad/chronomodel that referenced this pull request Jun 2, 2024
This patch is not required in Rails >= 7.1

Close #311

Ref:
- rails/rails#46864
- rails/rails@1d98bc5

[ci skip]
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.

None yet

4 participants