rooq 1.0.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 +7 -0
- data/.tool-versions +1 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +54 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +661 -0
- data/README.md +98 -0
- data/Rakefile +130 -0
- data/USAGE.md +850 -0
- data/exe/rooq +7 -0
- data/lib/rooq/adapters/postgresql.rb +117 -0
- data/lib/rooq/adapters.rb +3 -0
- data/lib/rooq/cli.rb +230 -0
- data/lib/rooq/condition.rb +104 -0
- data/lib/rooq/configuration.rb +56 -0
- data/lib/rooq/connection.rb +131 -0
- data/lib/rooq/context.rb +141 -0
- data/lib/rooq/dialect/base.rb +27 -0
- data/lib/rooq/dialect/postgresql.rb +531 -0
- data/lib/rooq/dialect.rb +9 -0
- data/lib/rooq/dsl/delete_query.rb +37 -0
- data/lib/rooq/dsl/insert_query.rb +43 -0
- data/lib/rooq/dsl/select_query.rb +301 -0
- data/lib/rooq/dsl/update_query.rb +44 -0
- data/lib/rooq/dsl.rb +28 -0
- data/lib/rooq/executor.rb +65 -0
- data/lib/rooq/expression.rb +494 -0
- data/lib/rooq/field.rb +71 -0
- data/lib/rooq/generator/code_generator.rb +91 -0
- data/lib/rooq/generator/introspector.rb +265 -0
- data/lib/rooq/generator.rb +9 -0
- data/lib/rooq/parameter_converter.rb +98 -0
- data/lib/rooq/query_validator.rb +176 -0
- data/lib/rooq/result.rb +248 -0
- data/lib/rooq/schema_validator.rb +56 -0
- data/lib/rooq/table.rb +69 -0
- data/lib/rooq/version.rb +5 -0
- data/lib/rooq.rb +25 -0
- data/rooq.gemspec +35 -0
- data/sorbet/config +4 -0
- metadata +115 -0
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# rOOQ
|
|
2
|
+
|
|
3
|
+
A Ruby query builder inspired by [jOOQ](https://www.jooq.org/). Build type-safe SQL queries using a fluent, chainable API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fluent Query Builder**: Chainable methods for building SELECT, INSERT, UPDATE, and DELETE queries
|
|
8
|
+
- **Immutable Queries**: Each builder method returns a new query object
|
|
9
|
+
- **Schema Validation**: Generate Ruby code from database schemas with runtime validation
|
|
10
|
+
- **PostgreSQL Support**: Full PostgreSQL dialect with parameterized queries
|
|
11
|
+
- **Advanced SQL Features**:
|
|
12
|
+
- DISTINCT, GROUP BY, HAVING
|
|
13
|
+
- Window functions (ROW_NUMBER, RANK, LAG, LEAD, etc.)
|
|
14
|
+
- Common Table Expressions (CTEs)
|
|
15
|
+
- Set operations (UNION, INTERSECT, EXCEPT)
|
|
16
|
+
- CASE WHEN expressions
|
|
17
|
+
- Aggregate functions (COUNT, SUM, AVG, MIN, MAX)
|
|
18
|
+
- Grouping sets (CUBE, ROLLUP, GROUPING SETS)
|
|
19
|
+
- **CLI Tool**: Generate schema files from the command line
|
|
20
|
+
- **Optional Sorbet Types**: Full type annotations with optional generation
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add this line to your application's Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem 'rooq'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
And then execute:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require 'rooq'
|
|
40
|
+
|
|
41
|
+
# Define a table
|
|
42
|
+
books = Rooq::Table.new(:books) do |t|
|
|
43
|
+
t.field :id, :integer
|
|
44
|
+
t.field :title, :string
|
|
45
|
+
t.field :author_id, :integer
|
|
46
|
+
t.field :published_in, :integer
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build a query
|
|
50
|
+
query = Rooq::DSL.select(books.TITLE, books.PUBLISHED_IN)
|
|
51
|
+
.from(books)
|
|
52
|
+
.where(books.PUBLISHED_IN.gte(2010))
|
|
53
|
+
.order_by(books.TITLE.asc)
|
|
54
|
+
.limit(10)
|
|
55
|
+
|
|
56
|
+
result = query.to_sql
|
|
57
|
+
# result.sql => "SELECT books.title, books.published_in FROM books WHERE books.published_in >= $1 ORDER BY books.title ASC LIMIT 10"
|
|
58
|
+
# result.params => [2010]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI Usage
|
|
62
|
+
|
|
63
|
+
Generate Ruby table definitions from your PostgreSQL database:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Generate schema to lib/schema.rb (default)
|
|
67
|
+
rooq generate -d myapp_development
|
|
68
|
+
|
|
69
|
+
# Generate with custom namespace (writes to lib/my_app/db.rb)
|
|
70
|
+
rooq generate -d myapp_development -n MyApp::DB
|
|
71
|
+
|
|
72
|
+
# Generate without Sorbet types
|
|
73
|
+
rooq generate -d myapp_development --no-typed
|
|
74
|
+
|
|
75
|
+
# Print to stdout instead of file
|
|
76
|
+
rooq generate -d myapp_development --stdout
|
|
77
|
+
|
|
78
|
+
# See all options
|
|
79
|
+
rooq help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
See [USAGE.md](USAGE.md) for detailed usage examples.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Install dependencies
|
|
90
|
+
bundle install
|
|
91
|
+
|
|
92
|
+
# Run tests
|
|
93
|
+
bundle exec rake test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
require "bundler/gem_tasks"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "find"
|
|
7
|
+
|
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
|
9
|
+
t.libs << "lib" << "test"
|
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
11
|
+
t.verbose = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
namespace :docs do
|
|
15
|
+
desc "Fix YARD documentation for subpath hosting"
|
|
16
|
+
task :fix do
|
|
17
|
+
require_relative "lib/rooq/version"
|
|
18
|
+
version = Rooq::VERSION
|
|
19
|
+
base_url = "https://ggalmazor.com/rooq/"
|
|
20
|
+
doc_dir = File.join(__dir__, "doc")
|
|
21
|
+
|
|
22
|
+
unless Dir.exist?(doc_dir)
|
|
23
|
+
puts "Error: #{doc_dir} not found. Run 'yard doc' first."
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 1. Fix app.js (the AJAX navigation issue)
|
|
28
|
+
app_js_path = File.join(doc_dir, "js", "app.js")
|
|
29
|
+
if File.exist?(app_js_path)
|
|
30
|
+
content = File.read(app_js_path)
|
|
31
|
+
search_pattern = /if \(e\.data\.action === "navigate"\) \{/
|
|
32
|
+
if content.match?(search_pattern)
|
|
33
|
+
puts "Applying fix to #{app_js_path}..."
|
|
34
|
+
|
|
35
|
+
replacement = <<~'JS'.chomp
|
|
36
|
+
if (e.data.action === "navigate") {
|
|
37
|
+
/*
|
|
38
|
+
When hosted at a subpath (e.g., https://ggalmazor.com/rooq/),
|
|
39
|
+
we need to ensure relative URLs from the iframe are resolved
|
|
40
|
+
against the documentation's root path, not the current browser URL
|
|
41
|
+
which might have changed due to pushState.
|
|
42
|
+
*/
|
|
43
|
+
const indexPage = window.location.pathname.indexOf('/index.html');
|
|
44
|
+
const rootPath = indexPage !== -1
|
|
45
|
+
? window.location.pathname.substring(0, indexPage + 1)
|
|
46
|
+
: window.location.pathname.replace(/\/[^\/]*$/, "/");
|
|
47
|
+
|
|
48
|
+
const baseUrl = window.location.origin + rootPath;
|
|
49
|
+
const url = new URL(e.data.url, baseUrl);
|
|
50
|
+
JS
|
|
51
|
+
|
|
52
|
+
content.gsub!(search_pattern, replacement)
|
|
53
|
+
content.gsub!("fetch(e.data.url)", "fetch(url.href)")
|
|
54
|
+
content.gsub!('history.pushState({}, document.title, e.data.url)', "history.pushState({}, document.title, url.href)")
|
|
55
|
+
redundant_url_parser = /const url = new URL\(e\.data\.url, "https:\/\/localhost"\);/
|
|
56
|
+
content.gsub!(redundant_url_parser, "")
|
|
57
|
+
|
|
58
|
+
File.write(app_js_path, content)
|
|
59
|
+
puts "Fix applied to app.js."
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 2. Convert all relative links to absolute links in HTML files and inject version
|
|
64
|
+
puts "Converting relative links to absolute and injecting version #{version} in #{doc_dir}..."
|
|
65
|
+
|
|
66
|
+
extra_files_mapping = {
|
|
67
|
+
"USAGE_md.html" => "file.USAGE.html",
|
|
68
|
+
"CHANGELOG_md.html" => "file.CHANGELOG.html"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Find.find(doc_dir) do |path|
|
|
72
|
+
next unless path.end_with?(".html")
|
|
73
|
+
|
|
74
|
+
content = File.read(path)
|
|
75
|
+
|
|
76
|
+
# Inject version into title if it's the standard "rOOQ Documentation"
|
|
77
|
+
content.gsub!("rOOQ Documentation", "rOOQ v#{version} Documentation")
|
|
78
|
+
|
|
79
|
+
# Inject version into the #menu element for better visibility
|
|
80
|
+
content.gsub!(/<div id="menu">/, "<div id=\"menu\">\n <span class=\"version\">v#{version}</span>")
|
|
81
|
+
|
|
82
|
+
# YARD uses relative paths like "", "Rooq", "Rooq/SomeSubClass"
|
|
83
|
+
# We want to find href="../something.html" and replace it with base_url + something.html
|
|
84
|
+
|
|
85
|
+
new_content = content.gsub(/(href|src)=["']([^"']+)["']/) do |match|
|
|
86
|
+
attr = ::Regexp.last_match(1)
|
|
87
|
+
url = ::Regexp.last_match(2)
|
|
88
|
+
|
|
89
|
+
# If it's an absolute URL starting with our base_url, we might need to fix it
|
|
90
|
+
if url.start_with?(base_url)
|
|
91
|
+
relative_part = url.sub(base_url, "")
|
|
92
|
+
if extra_files_mapping.key?(relative_part)
|
|
93
|
+
"#{attr}=\"#{base_url}#{extra_files_mapping[relative_part]}\""
|
|
94
|
+
else
|
|
95
|
+
match
|
|
96
|
+
end
|
|
97
|
+
# Skip other absolute URLs, anchors, and data URIs
|
|
98
|
+
elsif url.start_with?("http://", "https://", "#", "data:") || url.empty?
|
|
99
|
+
match
|
|
100
|
+
else
|
|
101
|
+
# Fix links to extra files that YARD renames
|
|
102
|
+
target_url = extra_files_mapping[url] || url
|
|
103
|
+
|
|
104
|
+
# If it's one of our extra files, it's at the root of doc/
|
|
105
|
+
absolute_file_path = if extra_files_mapping.key?(url)
|
|
106
|
+
File.join(File.expand_path(doc_dir), target_url)
|
|
107
|
+
else
|
|
108
|
+
File.expand_path(target_url, File.dirname(path))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if absolute_file_path.start_with?(File.expand_path(doc_dir))
|
|
112
|
+
relative_to_doc_root = absolute_file_path.sub("#{File.expand_path(doc_dir)}/", "")
|
|
113
|
+
# Handle index.html at root
|
|
114
|
+
relative_to_doc_root = "" if relative_to_doc_root == File.expand_path(doc_dir)
|
|
115
|
+
|
|
116
|
+
"#{attr}=\"#{base_url}#{relative_to_doc_root}\""
|
|
117
|
+
else
|
|
118
|
+
match
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
File.write(path, new_content) if new_content != content
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
puts "Finished converting links."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
task default: :test
|