Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rails/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,15 @@ def generate_column_content(name)
@response_builder.push(
model[:indexes].map do |index|
uniqueness = index[:unique] ? " (unique)" : ""
"- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
columns = case index[:columns]
when Array
index[:columns].join(",")
when String
index[:columns]
else
index[:name]
end
"- **#{index[:name]}** (#{columns})#{uniqueness}"
Comment on lines +100 to +108
Copy link
Member

Choose a reason for hiding this comment

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

Just trying to understand the different scenarios here. Array is the case when we have multiple columns associated to the index.

When is it a string? And when would it not be either (when do we reach the else block)?

In the case of the else block, we're essentially showing the index's name twice on the hover, which may not be super useful. Is there any other information for that scenario that we could show?

Copy link
Author

@cpgo cpgo Aug 18, 2025

Choose a reason for hiding this comment

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

I guess the else case is not really necessary, I just added because I'm not super familiar with the possible values here.

When is it a string?

I noticed on one of my applications when indexes contained "complex" sql processing like

execute "CREATE UNIQUE INDEX users_unique_complex ON users (COALESCE(country_id, 0), ltrim(first_name));"

In those cases .join was called on the whole index expression CREATE UNIQUE INDEX... instead of the array of columns

end.join("\n"),
category: :documentation,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddComplexIndexToUsers < ActiveRecord::Migration[8.0]
def change
execute "CREATE UNIQUE INDEX users_unique_complex ON users (COALESCE(country_id, 0), ltrim(first_name));"
end
end
3 changes: 2 additions & 1 deletion test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2024_10_25_225348) do
ActiveRecord::Schema[8.0].define(version: 2025_08_16_140957) do
create_table "composite_primary_keys", primary_key: ["order_id", "product_id"], force: :cascade do |t|
t.integer "order_id"
t.integer "product_id"
Expand Down Expand Up @@ -55,6 +55,7 @@
t.datetime "updated_at", null: false
t.integer "country_id", null: false
t.boolean "active", default: true, null: false
t.index "COALESCE(country_id, 0), ltrim(first_name)", name: "users_unique_complex", unique: true
t.index ["country_id"], name: "index_users_on_country_id"
end

Expand Down
63 changes: 63 additions & 0 deletions test/ruby_lsp_rails/hover_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,69 @@ class Bar < ApplicationRecord
CONTENT
end

test "handles complex indexes with expressions" do
expected_response = {
schema_file: "#{dummy_root}/db/schema.rb",
columns: [
["id", "integer", nil, false],
["first_name", "string", "", true],
["last_name", "string", nil, true],
["age", "integer", "0", true],
["created_at", "datetime", nil, false],
["updated_at", "datetime", nil, false],
["country_id", "integer", nil, false],
["active", "boolean", "true", false],
],
primary_keys: ["id"],
foreign_keys: ["country_id"],
indexes: [
{ name: "index_users_on_country_id", columns: ["country_id"], unique: false },
{ name: "users_unique_complex", columns: "COALESCE(country_id, 0), ltrim(first_name)", unique: true }
],
}

RunnerClient.any_instance.stubs(model: expected_response)

response = hover_on_source(<<~RUBY, { line: 3, character: 0 })
class User < ApplicationRecord
end

User
RUBY

assert_equal(<<~CONTENT.chomp, response.contents.value)
```ruby
User
```

**Definitions**: [fake.rb](file:///fake.rb#L1,1-2,4)


[Schema](#{URI::Generic.from_path(path: dummy_root + "/db/schema.rb")})

### Columns
- **id**: integer (PK)

- **first_name**: string - default: ""

- **last_name**: string

- **age**: integer - default: 0

- **created_at**: datetime - not null

- **updated_at**: datetime - not null

- **country_id**: integer (FK) - not null

- **active**: boolean - default: true - not null

### Indexes
- **index_users_on_country_id** (country_id)
- **users_unique_complex** (COALESCE(country_id, 0), ltrim(first_name)) (unique)
CONTENT
end

private

def hover_on_source(source, position)
Expand Down
Loading