rails-nl2sql 0.1.7 → 0.2.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: 9723feb23a2bc5f1100279427d18b106964382689ade9c37b379cda0274e0a77
4
- data.tar.gz: 9e4a429bc94b4f5d865e9efde2c19da5b6d9a775ffaf982e7ab1fe3070aa75f0
3
+ metadata.gz: c82ac012038c7065a0c3eeb3db0389fb64eec1fe6c95ae4c0896d6c580527dcc
4
+ data.tar.gz: 2a6236aad4a76959d360f6b19a7c7c38f662e66cfa7799108a4dd4e6cc546a7f
5
5
  SHA512:
6
- metadata.gz: 24b563aeed1df87e3afc4683230471e18351e364794989791dd28e2057d8db42cd3a083b678640ac74dfff6bb4d4c28573ea6efa955ee1faae29d1dd2fc3984a
7
- data.tar.gz: 74aa67f890208d440baaae2f863e6d92cc04d00c9ffc0012e14239e5f5bc0d2807cc170b460cba6c95c487812c9960209b6e1a7bf76ec238c8d63df342855e2b
6
+ metadata.gz: abe39e44ac4a2443b3cb778f7393cc012099c9266e1a35d25b3ad3bfdfae0b43b27a1206f52026f7cd31342931dc35ec0012ceb3bb8245a23fdadbaa9112da17
7
+ data.tar.gz: 0ab5536299aa7f4b7063c4c5264ec1b335a7e34957537c18da0bfa19d86b991efc135288d70be76938858e25ad9219d6f4f4d0b0299b72af212a7cb68ffa9418
data/.DS_Store ADDED
Binary file
data/README.md CHANGED
@@ -42,6 +42,16 @@ To execute a natural language query, you can use the `execute` method:
42
42
  results = Rails::Nl2sql::Processor.execute("Show me all the users from California")
43
43
  ```
44
44
 
45
+ ### Using `from_nl` with ActiveRecord
46
+
47
+ You can call the NL2SQL processor directly on your models. The `from_nl` method
48
+ returns an `ActiveRecord::Relation` so you can chain scopes, pagination and
49
+ other query modifiers as usual.
50
+
51
+ ```ruby
52
+ User.from_nl("all users who signed up last week").limit(10)
53
+ ```
54
+
45
55
  You can also specify which tables to include or exclude:
46
56
 
47
57
  ```ruby
@@ -60,6 +70,18 @@ Rails::Nl2sql::Processor.get_tables
60
70
  Rails::Nl2sql::Processor.get_schema(include: ["users", "orders"])
61
71
  ```
62
72
 
73
+ ### Schema caching
74
+
75
+ For efficiency the gem caches the full database schema on first use. The cached
76
+ schema is reused for subsequent requests so your application doesn't need to hit
77
+ the database every time a prompt is generated.
78
+
79
+ You can clear the cached schema if your database changes:
80
+
81
+ ```ruby
82
+ Rails::Nl2sql::SchemaBuilder.clear_cache!
83
+ ```
84
+
63
85
  ## Development
64
86
 
65
87
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module RailsNl2sql
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def copy_initializer
9
+ template 'rails_nl2sql.rb', 'config/initializers/rails_nl2sql.rb'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ Rails::Nl2sql.configure do |config|
2
+ config.api_key = "YOUR_API_KEY"
3
+ # config.model = "text-davinci-003"
4
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/lazy_load_hooks'
3
+
4
+ module Rails
5
+ module Nl2sql
6
+ module ActiveRecordExtension
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def from_nl(prompt, options = {})
11
+ sql = Rails::Nl2sql::Processor.generate_query_only(prompt, options)
12
+ from(Arel.sql("(#{sql}) AS #{table_name}"))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveSupport.on_load(:active_record) do
20
+ include Rails::Nl2sql::ActiveRecordExtension
21
+ end
@@ -11,6 +11,11 @@ module Rails
11
11
  def generate_query(prompt, schema, db_server = "PostgreSQL", tables = nil)
12
12
  retrieved_context = build_context(schema, tables)
13
13
 
14
+ # Debug: Let's see what schema context is being sent
15
+ puts "=== SCHEMA CONTEXT BEING SENT TO AI ==="
16
+ puts retrieved_context
17
+ puts "=== END SCHEMA CONTEXT ==="
18
+
14
19
  system_prompt = build_system_prompt(db_server, retrieved_context)
15
20
  user_prompt = build_user_prompt(prompt)
16
21
 
@@ -20,13 +25,16 @@ module Rails
20
25
  parameters: {
21
26
  model: @model,
22
27
  prompt: full_prompt,
23
- max_tokens: 300,
28
+ max_tokens: 500,
24
29
  temperature: 0.1
25
30
  }
26
31
  )
27
32
 
28
33
  generated_query = response.dig("choices", 0, "text")&.strip
29
34
 
35
+ # Clean up the response to remove markdown formatting
36
+ generated_query = clean_sql_response(generated_query)
37
+
30
38
  # Safety check
31
39
  validate_query_safety(generated_query)
32
40
 
@@ -80,9 +88,9 @@ module Rails
80
88
  ---
81
89
  **SQL GENERATION RULES:**
82
90
  1. **SQL Dialect:** All generated SQL must be valid **#{db_server} syntax**.
83
- * For limiting results, use `LIMIT` (e.g., `LIMIT 10`) instead of `TOP`.
84
- * Be mindful of #{db_server}'s specific function names (e.g., `COUNT(*)`, `MAX()`) and behaviors.
85
- * For subqueries that return a single value to be used in a `WHERE` clause, ensure they are correctly formatted for #{db_server}.
91
+ * For limiting results, use LIMIT (e.g., LIMIT 10) instead of TOP.
92
+ * Be mindful of #{db_server}'s specific function names (e.g., COUNT(*), MAX()) and behaviors.
93
+ * For subqueries that return a single value to be used in a WHERE clause, ensure they are correctly formatted for #{db_server}.
86
94
  2. **Schema Adherence:** Only use table names and column names that are explicitly present in the provided context. Do not invent names.
87
95
  3. **Valid JOIN Paths:** All `JOIN` operations must be based on valid foreign key relationships. The provided schema context explicitly details many of these.
88
96
  4. **Safety First:** Absolutely **DO NOT** generate any DDL (CREATE, ALTER, DROP) or DML (INSERT, UPDATE, DELETE) statements. Only `SELECT` queries are permitted.
@@ -92,7 +100,7 @@ module Rails
92
100
  * This is essential
93
101
  6. **Ambiguity:** If a user question is ambiguous or requires more information to form a precise SQL query, clearly state that you need clarification and ask for more details. Do not guess.
94
102
 
95
- **RESPOND WITH ONLY THE SQL QUERY - NO EXPLANATIONS OR ADDITIONAL TEXT.**
103
+ **RESPOND WITH ONLY THE SQL QUERY - NO EXPLANATIONS, NO MARKDOWN FORMATTING, NO CODE BLOCKS, NO ADDITIONAL TEXT.**
96
104
  PROMPT
97
105
  end
98
106
 
@@ -103,6 +111,45 @@ module Rails
103
111
  PROMPT
104
112
  end
105
113
 
114
+ def clean_sql_response(query)
115
+ return query unless query
116
+
117
+ # Remove markdown code blocks
118
+ query = query.gsub(/```sql\n?/, '')
119
+ query = query.gsub(/```\n?/, '')
120
+
121
+ # Remove any leading/trailing whitespace
122
+ query = query.strip
123
+
124
+ # Remove any explanatory text before or after the query
125
+ # Look for common patterns like "Here's the SQL query:" or "The query is:"
126
+ query = query.gsub(/^.*?(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i, '\1')
127
+
128
+ # Remove any trailing explanatory text after the query
129
+ # Split by newlines and take only the SQL part
130
+ lines = query.split("\n")
131
+ sql_lines = []
132
+
133
+ lines.each do |line|
134
+ line = line.strip
135
+ # Skip empty lines or lines that look like explanations
136
+ next if line.empty?
137
+ next if line.match(/^(here|this|the query|explanation|note)/i)
138
+
139
+ sql_lines << line
140
+ end
141
+
142
+ # Rejoin the SQL lines
143
+ cleaned_query = sql_lines.join("\n").strip
144
+
145
+ # Ensure it ends with a semicolon if it's a complete query
146
+ if cleaned_query.match(/^(SELECT|WITH)/i) && !cleaned_query.end_with?(';')
147
+ cleaned_query += ';'
148
+ end
149
+
150
+ cleaned_query
151
+ end
152
+
106
153
  def validate_query_safety(query)
107
154
  return unless query
108
155
 
@@ -2,17 +2,43 @@ module Rails
2
2
  module Nl2sql
3
3
  class QueryValidator
4
4
  def self.validate(query)
5
+ return false unless query && !query.strip.empty?
6
+
7
+ # Clean the query first
8
+ query = query.strip
9
+
10
+ # Check if query is malformed (contains markdown or other formatting)
11
+ if query.include?('```') || query.include?('```sql')
12
+ raise Rails::Nl2sql::Error, "Query contains markdown formatting and could not be cleaned properly"
13
+ end
14
+
5
15
  # Basic validation: prevent destructive commands
6
- disallowed_keywords = %w(DROP DELETE UPDATE INSERT TRUNCATE ALTER CREATE)
7
- if disallowed_keywords.any? { |keyword| query.upcase.include?(keyword) }
8
- raise "Query contains disallowed keywords."
16
+ disallowed_keywords = %w(DROP DELETE UPDATE INSERT TRUNCATE ALTER CREATE EXEC EXECUTE MERGE REPLACE)
17
+ query_upper = query.upcase
18
+
19
+ if disallowed_keywords.any? { |keyword| query_upper.include?(keyword) }
20
+ raise Rails::Nl2sql::Error, "Query contains disallowed keywords."
21
+ end
22
+
23
+ # Ensure there is only a single statement
24
+ cleaned_query = query.rstrip
25
+ cleaned_query = cleaned_query.chomp(';')
26
+ if cleaned_query.include?(';')
27
+ raise Rails::Nl2sql::Error, "Query contains multiple statements."
28
+ end
29
+
30
+ # Ensure it's a SELECT query
31
+ unless query_upper.strip.start_with?('SELECT', 'WITH')
32
+ raise Rails::Nl2sql::Error, "Only SELECT queries are allowed."
9
33
  end
10
34
 
11
- # Use Rails' built-in sanitization to be safe
35
+ # Use Rails' built-in validation with EXPLAIN
12
36
  begin
13
- ActiveRecord::Base.connection.execute("EXPLAIN #{query}")
37
+ # Remove trailing semicolon for EXPLAIN
38
+ explain_query = query.gsub(/;\s*$/, '')
39
+ ActiveRecord::Base.connection.execute("EXPLAIN #{explain_query}")
14
40
  rescue ActiveRecord::StatementInvalid => e
15
- raise "Invalid SQL query: #{e.message}"
41
+ raise Rails::Nl2sql::Error, "Invalid SQL query: #{e.message}"
16
42
  end
17
43
 
18
44
  true
@@ -1,13 +1,26 @@
1
1
  module Rails
2
2
  module Nl2sql
3
3
  class SchemaBuilder
4
+ @@schema_cache = nil
5
+
4
6
  def self.build_schema(options = {})
7
+ if options.empty? && @@schema_cache
8
+ return @@schema_cache
9
+ end
10
+
5
11
  tables = get_filtered_tables(options)
6
-
12
+
7
13
  schema_text = build_schema_text(tables)
14
+
15
+ @@schema_cache = schema_text if options.empty?
16
+
8
17
  schema_text
9
18
  end
10
19
 
20
+ def self.clear_cache!
21
+ @@schema_cache = nil
22
+ end
23
+
11
24
  def self.get_database_type
12
25
  adapter = ActiveRecord::Base.connection.adapter_name.downcase
13
26
  case adapter
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Nl2sql
3
- VERSION = "0.1.7"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/rails/nl2sql.rb CHANGED
@@ -2,6 +2,7 @@ require "rails/nl2sql/version"
2
2
  require "rails/nl2sql/query_generator"
3
3
  require "rails/nl2sql/schema_builder"
4
4
  require "rails/nl2sql/query_validator"
5
+ require "rails/nl2sql/active_record_extension"
5
6
  require "rails/nl2sql/railtie" if defined?(Rails)
6
7
 
7
8
  module Rails
@@ -26,6 +27,11 @@ module Rails
26
27
  # Build schema with optional table filtering
27
28
  schema = SchemaBuilder.build_schema(options)
28
29
 
30
+ # Debug: Show what schema is being built
31
+ puts "=== RAW SCHEMA FROM BUILDER ==="
32
+ puts schema
33
+ puts "=== END RAW SCHEMA ==="
34
+
29
35
  # Extract tables for filtering if specified
30
36
  tables = options[:tables]
31
37
 
@@ -0,0 +1,2 @@
1
+ require "rails/nl2sql"
2
+
data/rails-nl2sql.gemspec CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
37
37
  spec.require_paths = ["lib"]
38
38
 
39
39
  spec.add_dependency "openai", "~> 0.3"
40
- spec.add_development_dependency "bundler", "~> 1.17"
40
+ spec.add_development_dependency "bundler", ">= 2.0"
41
41
  spec.add_development_dependency "rake", "~> 10.0"
42
42
  spec.add_development_dependency "rspec-rails", "~> 6.0"
43
43
  spec.add_dependency "railties", ">= 6.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-nl2sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Russell Van Curen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-17 00:00:00.000000000 Z
11
+ date: 2025-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: openai
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.17'
33
+ version: '2.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.17'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -89,6 +89,7 @@ executables: []
89
89
  extensions: []
90
90
  extra_rdoc_files: []
91
91
  files:
92
+ - ".DS_Store"
92
93
  - ".gitignore"
93
94
  - Gemfile
94
95
  - Gemfile.lock
@@ -99,7 +100,11 @@ files:
99
100
  - bin/setup
100
101
  - lib/generators/rails/nl2sql/install_generator.rb
101
102
  - lib/generators/rails/nl2sql/templates/rails_nl2sql.rb
103
+ - lib/generators/rails_nl2sql/install/install_generator.rb
104
+ - lib/generators/rails_nl2sql/install/templates/rails_nl2sql.rb
105
+ - lib/rails-nl2sql.rb
102
106
  - lib/rails/nl2sql.rb
107
+ - lib/rails/nl2sql/active_record_extension.rb
103
108
  - lib/rails/nl2sql/query_generator.rb
104
109
  - lib/rails/nl2sql/query_validator.rb
105
110
  - lib/rails/nl2sql/railtie.rb