tidewave 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d4f421f00436367f21d189e0fe49ea8a1ce4f611457b720f62027b4b8f080dc
4
- data.tar.gz: 6bb95b976f34b5520b54fb8a0789d1adf18c591cef285bcd5500f591b2c4d1a3
3
+ metadata.gz: d140360315f54166fc14e330f33fc5dd62e561c49ba3d6cb5044a8e7b5247e41
4
+ data.tar.gz: c69a1a0363ce0f50a396599f2b551896a602431929731dd9fbf4fbe283823cf1
5
5
  SHA512:
6
- metadata.gz: 851460abeef4e3d29d0d475a83f1e962bb0a6279708e25cc25ed2b8620782e48f837f8db279fac1da822af3c1d5472208be2340ca691ef5a226ab7bd7f4292f9
7
- data.tar.gz: 48f5e731497bf822329c2de6865361c0d31bfe5fa64afed0c213d75d8de4db08ed4f44aa7ef76e889803330c70ba202712f724d569650d2707dccd3debb74d11
6
+ metadata.gz: 6f3203bbce1c31681fcb2b21575ea62a8714a469f61ec30be8d59ccd22028592b7a33317b4f434e0d7a4051caaf1db1aa82247d395a961d4a09f1a1538cc6cd8
7
+ data.tar.gz: 968742eed4690b5c2a1f8c0ecf233e4742a19bc2ac8a700d4f64eca2878bea8f70d1d2228cffc472019462790eb122601b7acfc608fd7950c100974f3c9053d4
data/README.md CHANGED
@@ -1,21 +1,8 @@
1
1
  # Tidewave
2
2
 
3
- Tidewave speeds up development with an AI assistant that understands your web application,
4
- how it runs, and what it delivers. Our current release connects your editor's
5
- assistant to your web framework runtime via [MCP](https://modelcontextprotocol.io/).
3
+ Tidewave is the coding agent for full-stack web app development, deeply integrated with Rails, from the database to the UI. [See our website](https://tidewave.ai) for more information.
6
4
 
7
- [See our website](https://tidewave.ai) for more information.
8
-
9
- ## Key Features
10
-
11
- Tidewave provides tools that allow your LLM of choice to:
12
-
13
- - inspect your application logs to help debugging errors
14
- - execute SQL queries and inspect your database
15
- - evaluate custom Ruby code in the context of your project
16
- - find Rubygems packages and source code locations
17
-
18
- and more.
5
+ This project can also be used as a standalone Model Context Protocol server for your editors.
19
6
 
20
7
  ## Installation
21
8
 
@@ -25,40 +12,52 @@ You can install Tidewave by adding the `tidewave` gem to the development group i
25
12
  gem "tidewave", group: :development
26
13
  ```
27
14
 
28
- Tidewave will now run on the same port as your regular Rails application.
29
- In particular, the MCP is located by default at http://localhost:3000/tidewave/mcp.
30
- [You must configure your editor and AI assistants accordingly](https://hexdocs.pm/tidewave/mcp.html).
15
+ Now access `/tidewave` route of your web application to enjoy Tidewave Web!
31
16
 
32
- ## Configuration
17
+ ## Troubleshooting
18
+
19
+ ### Localhost requirement
33
20
 
34
- You may configure the `tidewave` using the following syntax:
21
+ Tidewave expects your web application to be running on `localhost`. If you are not running on localhost, you may need to set some additional configuration. In particular, you must configure Tidewave to allow `allow_remote_access` and [optionally configure your Rails hosts](https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization). For example, in your `config/environments/development.rb`:
35
22
 
36
23
  ```ruby
37
- config.tidewave.allowed_ips = [IPAddr.new("192.168.97.1")]
24
+ config.hosts << "company.local"
25
+ config.tidewave.allow_remote_access = true
38
26
  ```
39
27
 
40
- The following options are available:
28
+ If you want to use Docker for development, you either need to enable the configuration above or automatically redirect the relevant ports, as done by [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers). See our [containers](https://hexdocs.pm/tidewave/containers.html) guide for more information.
41
29
 
42
- * `:allowed_origins`
30
+ ### Content security policy
43
31
 
44
- * `:localhost_only`
32
+ If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly.
45
33
 
46
- * `:allowed_ips`
34
+ ### Web server requirements
47
35
 
48
- You can read more about this options in [FastMCP](https://github.com/yjacquin/fast_mcp) README.
36
+ At the moment, Tidewave requires all requests to be processed by the same process. In case Tidewave cannot connect to your application, consider starting your Rails application as follows:
49
37
 
50
- ## Considerations
38
+ RAILS_MAX_THREADS=1 WEB_CONCURRENCY=1 rails server
51
39
 
52
40
  ### Production Environment
53
41
 
54
- Tidewave is a powerful tool that can help you develop your web application faster and more efficiently.
55
- However, it is important to note that Tidewave is not meant to be used in a production environment.
42
+ Tidewave is a powerful tool that can help you develop your web application faster and more efficiently. However, it is important to note that Tidewave is not meant to be used in a production environment.
56
43
 
57
- Tidewave will raise an error if it is used in a production environment.
44
+ Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
58
45
 
59
- ### Web server requirements
46
+ ## Configuration
47
+
48
+ You may configure `tidewave` using the following syntax:
49
+
50
+ ```ruby
51
+ config.tidewave.allow_remote_access = true
52
+ ```
53
+
54
+ The following config is available:
55
+
56
+ * `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces as well. If you trust your network and need to access Tidewave from a different machine, this configuration can be set to `true`
57
+
58
+ * `preferred_orm` - which ORM to use, either `:active_record` (default) or `:sequel`
60
59
 
61
- Tidewave currently requires a threaded web server like Puma.
60
+ * `team` - set your team configuration, such as `config.tidewave.team = { id: "my-company }`
62
61
 
63
62
  ## Acknowledgements
64
63
 
@@ -2,13 +2,15 @@
2
2
 
3
3
  module Tidewave
4
4
  class Configuration
5
- attr_accessor :logger, :allowed_origins, :localhost_only, :allowed_ips
5
+ attr_accessor :logger, :allow_remote_access, :preferred_orm, :credentials, :client_url, :team
6
6
 
7
7
  def initialize
8
- @logger = Logger.new(STDOUT)
9
- @allowed_origins = nil
10
- @localhost_only = true
11
- @allowed_ips = nil
8
+ @logger = nil
9
+ @allow_remote_access = true
10
+ @preferred_orm = :active_record
11
+ @credentials = {}
12
+ @client_url = "https://tidewave.ai"
13
+ @team = {}
12
14
  end
13
15
  end
14
16
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tidewave
4
+ class DatabaseAdapter
5
+ class << self
6
+ def current
7
+ @current ||= create_adapter
8
+ end
9
+
10
+ def create_adapter
11
+ orm_type = Rails.application.config.tidewave.preferred_orm
12
+ case orm_type
13
+ when :active_record
14
+ require_relative "database_adapters/active_record"
15
+ DatabaseAdapters::ActiveRecord.new
16
+ when :sequel
17
+ require_relative "database_adapters/sequel"
18
+ DatabaseAdapters::Sequel.new
19
+ else
20
+ raise "Unknown preferred ORM: #{orm_type}"
21
+ end
22
+ end
23
+ end
24
+
25
+ def execute_query(query, arguments = [])
26
+ raise NotImplementedError, "Subclasses must implement execute_query"
27
+ end
28
+
29
+ def get_base_class
30
+ raise NotImplementedError, "Subclasses must implement get_base_class"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tidewave
4
+ module DatabaseAdapters
5
+ class ActiveRecord < DatabaseAdapter
6
+ RESULT_LIMIT = 50
7
+
8
+ def execute_query(query, arguments = [])
9
+ conn = ::ActiveRecord::Base.connection
10
+
11
+ # Execute the query with prepared statement and arguments
12
+ if arguments.any?
13
+ result = conn.exec_query(query, "SQL", arguments)
14
+ else
15
+ result = conn.exec_query(query)
16
+ end
17
+
18
+ # Format the result
19
+ {
20
+ columns: result.columns,
21
+ rows: result.rows.first(RESULT_LIMIT),
22
+ row_count: result.rows.length,
23
+ adapter: conn.adapter_name,
24
+ database: database_name
25
+ }
26
+ end
27
+
28
+ def get_base_class
29
+ ::ActiveRecord::Base
30
+ end
31
+
32
+ private
33
+
34
+ def database_name
35
+ Rails.configuration.database_configuration.dig(Rails.env, "database")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tidewave
4
+ module DatabaseAdapters
5
+ class Sequel < DatabaseAdapter
6
+ RESULT_LIMIT = 50
7
+
8
+ def execute_query(query, arguments = [])
9
+ db = ::Sequel::Model.db
10
+
11
+ # Execute the query with arguments
12
+ result = if arguments.any?
13
+ db.fetch(query, *arguments)
14
+ else
15
+ db.fetch(query)
16
+ end
17
+
18
+ # Convert to array of hashes and extract metadata
19
+ rows = result.all
20
+ columns = rows.first&.keys || []
21
+
22
+ # Format the result similar to ActiveRecord
23
+ {
24
+ columns: columns.map(&:to_s),
25
+ rows: rows.first(RESULT_LIMIT).map(&:values),
26
+ row_count: rows.length,
27
+ adapter: db.adapter_scheme.to_s.upcase,
28
+ database: db.opts[:database]
29
+ }
30
+ end
31
+
32
+ def get_base_class
33
+ ::Sequel::Model
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tidewave::ExceptionsMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ request = ActionDispatch::Request.new(env)
10
+ status, headers, body = @app.call(env)
11
+
12
+ exception = request.get_header("tidewave.exception")
13
+
14
+ if exception
15
+ formatted = format_exception_html(exception, request)
16
+ body, headers = append_body(body, headers, formatted)
17
+ end
18
+
19
+ [ status, headers, body ]
20
+ rescue => error
21
+ Rails.logger.error("Failure in Tidewave::ExceptionsMiddleware, message: #{error.message}")
22
+ raise error
23
+ end
24
+
25
+ private
26
+
27
+ def format_exception_html(exception, request)
28
+ backtrace = Rails.backtrace_cleaner.clean(exception.backtrace)
29
+
30
+ parameters = request_parameters(request)
31
+
32
+ text = exception.class.name.dup
33
+
34
+ if parameters["controller"]
35
+ text << " in #{parameters["controller"].camelize}Controller"
36
+
37
+ if parameters["action"]
38
+ text << "##{parameters["action"]}"
39
+ end
40
+ end
41
+
42
+ text << "\n\n## Message\n\n#{exception.message}"
43
+
44
+ if backtrace.any?
45
+ text << "\n\n## Backtrace\n\n#{backtrace.join("\n")}"
46
+ end
47
+
48
+ text << "\n\n"
49
+
50
+ text << <<~TEXT.chomp
51
+ ## Request info
52
+
53
+ * URI: #{request.base_url + request.path}
54
+ * Query string: #{request.query_string}
55
+
56
+ ## Session
57
+
58
+ #{request.session.to_hash.inspect}
59
+ TEXT
60
+
61
+ %Q(<textarea style="display: none;" data-tidewave-exception-info>#{ERB::Util.html_escape(text)}</textarea>)
62
+ end
63
+
64
+ def request_parameters(request)
65
+ request.parameters
66
+ rescue ActionController::BadRequest
67
+ {}
68
+ end
69
+
70
+ def append_body(body, headers, content)
71
+ new_body = "".dup
72
+
73
+ body.each { |part| new_body << part }
74
+ body.close if body.respond_to?(:close)
75
+
76
+ if position = new_body.rindex("</body>")
77
+ new_body.insert(position, content)
78
+ else
79
+ new_body << content
80
+ end
81
+
82
+ if headers[Rack::CONTENT_LENGTH]
83
+ headers[Rack::CONTENT_LENGTH] = new_body.bytesize.to_s
84
+ end
85
+
86
+ [ [ new_body ], headers ]
87
+ end
88
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "ipaddr"
5
+ require "fast_mcp"
6
+ require "rack/request"
7
+ require "active_support/core_ext/class"
8
+ require "active_support/core_ext/object/blank"
9
+ require "json"
10
+ require "erb"
11
+
12
+ class Tidewave::Middleware
13
+ TIDEWAVE_ROUTE = "tidewave".freeze
14
+ EMPTY_ROUTE = "empty".freeze
15
+ SSE_ROUTE = "mcp".freeze
16
+ MESSAGES_ROUTE = "mcp/message".freeze
17
+ SHELL_ROUTE = "shell".freeze
18
+
19
+ INVALID_IP = <<~TEXT.freeze
20
+ For security reasons, Tidewave does not accept remote connections by default.
21
+
22
+ If you really want to allow remote connections, set `config.tidewave.allow_remote_access = true`.
23
+ TEXT
24
+
25
+ def initialize(app, config)
26
+ @allow_remote_access = config.allow_remote_access
27
+ @client_url = config.client_url
28
+ @team = config.team
29
+ @project_name = Rails.application.class.module_parent.name
30
+
31
+ @app = FastMcp.rack_middleware(app,
32
+ name: "tidewave",
33
+ version: Tidewave::VERSION,
34
+ path_prefix: "/" + TIDEWAVE_ROUTE,
35
+ messages_route: MESSAGES_ROUTE,
36
+ sse_route: SSE_ROUTE,
37
+ logger: config.logger || Logger.new(Rails.root.join("log", "tidewave.log")),
38
+ # Rails runs the HostAuthorization in dev, so we skip this
39
+ allowed_origins: [],
40
+ # We validate this one in Tidewave::Middleware
41
+ localhost_only: false
42
+ ) do |server|
43
+ server.filter_tools do |request, tools|
44
+ if request.params["include_fs_tools"] != "true"
45
+ tools.reject { |tool| tool.tags.include?(:file_system_tool) }
46
+ else
47
+ tools
48
+ end
49
+ end
50
+
51
+ server.register_tools(*Tidewave::Tools::Base.descendants)
52
+ end
53
+ end
54
+
55
+ def call(env)
56
+ request = Rack::Request.new(env)
57
+ path = request.path.split("/").reject(&:empty?)
58
+
59
+ if path[0] == TIDEWAVE_ROUTE
60
+ return forbidden(INVALID_IP) unless valid_client_ip?(request)
61
+
62
+ # The MCP routes are handled downstream by FastMCP
63
+ case [ request.request_method, path ]
64
+ when [ "GET", [ TIDEWAVE_ROUTE ] ]
65
+ return home(request)
66
+ when [ "GET", [ TIDEWAVE_ROUTE, EMPTY_ROUTE ] ]
67
+ return empty(request)
68
+ when [ "POST", [ TIDEWAVE_ROUTE, SHELL_ROUTE ] ]
69
+ return shell(request)
70
+ end
71
+ end
72
+
73
+ @app.call(env)
74
+ end
75
+
76
+ private
77
+
78
+ def home(request)
79
+ config = {
80
+ "project_name" => @project_name,
81
+ "framework_type" => "rails",
82
+ "tidewave_version" => Tidewave::VERSION,
83
+ "team" => @team
84
+ }
85
+
86
+ html = <<~HTML
87
+ <html>
88
+ <head>
89
+ <meta charset="UTF-8" />
90
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
91
+ <meta name="tidewave:config" content="#{ERB::Util.html_escape(JSON.generate(config))}" />
92
+ <script type="module" src="#{@client_url}/tc/tc.js"></script>
93
+ </head>
94
+ <body></body>
95
+ </html>
96
+ HTML
97
+
98
+ [ 200, { "Content-Type" => "text/html" }, [ html ] ]
99
+ end
100
+
101
+ def empty(request)
102
+ html = ""
103
+ [ 200, { "Content-Type" => "text/html" }, [ html ] ]
104
+ end
105
+
106
+ def forbidden(message)
107
+ Rails.logger.warn(message)
108
+ [ 403, { "Content-Type" => "text/plain" }, [ message ] ]
109
+ end
110
+
111
+ def shell(request)
112
+ body = request.body.read
113
+ return [ 400, { "Content-Type" => "text/plain" }, [ "Command body is required" ] ] if body.blank?
114
+
115
+ begin
116
+ parsed_body = JSON.parse(body)
117
+ cmd = parsed_body["command"]
118
+ return [ 400, { "Content-Type" => "text/plain" }, [ "Command field is required" ] ] if cmd.blank?
119
+ rescue JSON::ParserError
120
+ return [ 400, { "Content-Type" => "text/plain" }, [ "Invalid JSON in request body" ] ]
121
+ end
122
+
123
+ response = Rack::Response.new
124
+ response.status = 200
125
+ response.headers["Content-Type"] = "text/plain"
126
+
127
+ response.finish do |res|
128
+ begin
129
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
130
+ stdin.close
131
+
132
+ # Merge stdout and stderr streams
133
+ ios = [ stdout, stderr ]
134
+
135
+ until ios.empty?
136
+ ready = IO.select(ios, nil, nil, 0.1)
137
+ next unless ready
138
+
139
+ ready[0].each do |io|
140
+ begin
141
+ data = io.read_nonblock(4096)
142
+ if data
143
+ # Write binary chunk: type (0 for data) + 4-byte length + data
144
+ chunk = [ 0, data.bytesize ].pack("CN") + data
145
+ res.write(chunk)
146
+ end
147
+ rescue IO::WaitReadable
148
+ # No data available right now
149
+ rescue EOFError
150
+ # Stream ended
151
+ ios.delete(io)
152
+ end
153
+ end
154
+ end
155
+
156
+ # Wait for process to complete and get exit status
157
+ exit_status = wait_thr.value.exitstatus
158
+ status_json = JSON.generate({ status: exit_status })
159
+ # Write binary chunk: type (1 for status) + 4-byte length + JSON data
160
+ chunk = [ 1, status_json.bytesize ].pack("CN") + status_json
161
+ res.write(chunk)
162
+ end
163
+ rescue => e
164
+ error_json = JSON.generate({ status: 213 })
165
+ chunk = [ 1, error_json.bytesize ].pack("CN") + error_json
166
+ res.write(chunk)
167
+ end
168
+ end
169
+ end
170
+
171
+ def valid_client_ip?(request)
172
+ return true if @allow_remote_access
173
+
174
+ ip = request.ip
175
+ return false unless ip
176
+
177
+ addr = IPAddr.new(ip)
178
+
179
+ addr.loopback? ||
180
+ addr == IPAddr.new("127.0.0.1") ||
181
+ addr == IPAddr.new("::1") ||
182
+ addr == IPAddr.new("::ffff:127.0.0.1") # IPv4-mapped IPv6
183
+ end
184
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tidewave::QuietRequestsMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env["PATH_INFO"].start_with?("/tidewave")
10
+ Rails.logger.silence { @app.call(env) }
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
@@ -1,49 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fast_mcp"
4
3
  require "logger"
5
4
  require "fileutils"
6
5
  require "tidewave/configuration"
7
- require "active_support/core_ext/class"
6
+ require "tidewave/middleware"
7
+ require "tidewave/exceptions_middleware"
8
+ require "tidewave/quiet_requests_middleware"
8
9
 
9
10
  gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
10
11
  Dir[gem_tools_path].each { |f| require f }
11
12
 
12
13
  module Tidewave
13
14
  class Railtie < Rails::Railtie
14
- config.tidewave = Tidewave::Configuration.new
15
-
16
- initializer "tidewave.setup_mcp" do |app|
17
- # Prevent MCP server from being mounted if Rails is not running in development mode
18
- raise "For security reasons, Tidewave is only supported in development mode" unless Rails.env.development?
19
-
20
- config = app.config.tidewave
21
-
22
- # Set up MCP server with the host application
23
- FastMcp.mount_in_rails(
24
- app,
25
- name: "tidewave",
26
- version: Tidewave::VERSION,
27
- path_prefix: Tidewave::PATH_PREFIX,
28
- messages_route: Tidewave::MESSAGES_ROUTE,
29
- sse_route: Tidewave::SSE_ROUTE,
30
- logger: config.logger,
31
- allowed_origins: config.allowed_origins,
32
- localhost_only: config.localhost_only,
33
- allowed_ips: config.allowed_ips
34
- ) do |server|
35
- app.config.before_initialize do
36
- server.filter_tools do |request, tools|
37
- if request.params["include_fs_tools"] != "true"
38
- tools.reject { |tool| tool.tags.include?(:file_system_tool) }
39
- else
40
- tools
41
- end
42
- end
15
+ config.tidewave = Tidewave::Configuration.new()
16
+
17
+ initializer "tidewave.setup" do |app|
18
+ unless app.config.enable_reloading
19
+ raise "For security reasons, Tidewave is only supported in environments where config.enable_reloading is true (typically development)"
20
+ end
43
21
 
44
- server.register_tools(*Tidewave::Tools::Base.descendants)
22
+ app.config.middleware.insert_after(
23
+ ActionDispatch::Callbacks,
24
+ Tidewave::Middleware,
25
+ app.config.tidewave
26
+ )
27
+
28
+ app.config.after_initialize do
29
+ # If the user configured CSP, we need to alter it in dev
30
+ # to allow TC to run browser_eval.
31
+ app.config.content_security_policy.try do |content_security_policy|
32
+ content_security_policy.directives["script-src"].try do |script_src|
33
+ script_src << "'unsafe-eval'" unless script_src.include?("'unsafe-eval'")
34
+ end
45
35
  end
46
36
  end
47
37
  end
38
+
39
+ initializer "tidewave.intercept_exceptions" do |app|
40
+ # We intercept exceptions from DebugExceptions, format the
41
+ # information as text and inject into the exception page html.
42
+
43
+ ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
44
+ request.set_header("tidewave.exception", exception)
45
+ end
46
+
47
+ app.middleware.insert_before(ActionDispatch::DebugExceptions, Tidewave::ExceptionsMiddleware)
48
+ end
49
+
50
+ initializer "tidewave.logging" do |app|
51
+ # Do not pollute user logs with tidewave requests.
52
+ app.middleware.insert_before(Rails::Rack::Logger, Tidewave::QuietRequestsMiddleware)
53
+ end
48
54
  end
49
55
  end
@@ -3,7 +3,7 @@
3
3
  class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
4
4
  tool_name "execute_sql_query"
5
5
  description <<~DESCRIPTION
6
- Executes the given SQL query against the ActiveRecord database connection.
6
+ Executes the given SQL query against the database connection.
7
7
  Returns the result as a Ruby data structure.
8
8
 
9
9
  Note that the output is limited to 50 rows at a time. If you need to see more, perform additional calls
@@ -25,24 +25,6 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
25
25
  RESULT_LIMIT = 50
26
26
 
27
27
  def call(query:, arguments: [])
28
- # Get the ActiveRecord connection
29
- conn = ActiveRecord::Base.connection
30
-
31
- # Execute the query with prepared statement and arguments
32
- if arguments.any?
33
- result = conn.exec_query(query, "SQL", arguments)
34
- else
35
- result = conn.exec_query(query)
36
- end
37
-
38
-
39
- # Format the result
40
- {
41
- columns: result.columns,
42
- rows: result.rows.first(RESULT_LIMIT),
43
- row_count: result.rows.length,
44
- adapter: conn.adapter_name,
45
- database: Rails.configuration.database_configuration.dig(Rails.env, "database")
46
- }
28
+ Tidewave::DatabaseAdapter.current.execute_query(query, arguments)
47
29
  end
48
30
  end