Skip to content

Commit b5a238d

Browse files
committed
Add DuckDB adapter supporting modules
- Add quoting.rb for proper SQL quoting and escaping - Add explain_pretty_printer.rb for formatted EXPLAIN output - Add tasks.rb for Rails database tasks (db:create, db:drop, etc.)
1 parent ba51cc3 commit b5a238d

File tree

3 files changed

+429
-0
lines changed

3 files changed

+429
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
module Duckdb
6+
class ExplainPrettyPrinter # :nodoc:
7+
# @note Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles the output of the SQLite shell
8+
# @example Output format
9+
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
10+
# 0|1|1|SCAN TABLE posts (~100000 rows)
11+
# @param [ActiveRecord::Result] result Query result containing explain output
12+
# @return [String] Pretty-printed explanation with newlines
13+
def pp(result)
14+
result.rows.map do |row|
15+
row.join("|")
16+
end.join("\n") + "\n"
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
module Duckdb
6+
module Quoting # :nodoc:
7+
extend ActiveSupport::Concern
8+
9+
QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
10+
QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
11+
12+
module ClassMethods # :nodoc:
13+
# @note regex pattern for column name matching
14+
# @return [Regexp] Regular expression for matching column names
15+
def column_name_matcher
16+
/
17+
\A
18+
(
19+
(?:
20+
# "table_name"."column_name" | function(one or no argument)
21+
((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
22+
)
23+
(?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
24+
)
25+
(?:\s*,\s*\g<1>)*
26+
\z
27+
/ix
28+
end
29+
30+
# @note regex pattern for column name with order matching
31+
# @return [Regexp] Regular expression for matching column names with order
32+
def column_name_with_order_matcher
33+
/
34+
\A
35+
(
36+
(?:
37+
# "table_name"."column_name" | function(one or no argument)
38+
((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
39+
)
40+
(?:\s+COLLATE\s+(?:\w+|"\w+"))?
41+
(?:\s+ASC|\s+DESC)?
42+
)
43+
(?:\s*,\s*\g<1>)*
44+
\z
45+
/ix
46+
end
47+
48+
# @override
49+
# @note Implements AbstractAdapter interface method
50+
# @param [String, Symbol] name Column name to quote
51+
# @return [String] Quoted column name
52+
def quote_column_name(name)
53+
QUOTED_COLUMN_NAMES[name] ||= %Q("#{name.to_s.gsub('"', '""')}").freeze
54+
end
55+
56+
# @override
57+
# @note Implements AbstractAdapter interface method
58+
# @param [String, Symbol] name Table name to quote
59+
# @return [String] Quoted table name
60+
def quote_table_name(name)
61+
QUOTED_TABLE_NAMES[name] ||= %Q("#{name.to_s.gsub('"', '""').gsub(".", "\".\"")}").freeze
62+
end
63+
end
64+
65+
# @override
66+
# @note Implements AbstractAdapter interface method
67+
# @param [String] s String to quote
68+
# @return [String] Quoted string with escaped single quotes
69+
def quote_string(s)
70+
s.gsub("'", "''") # Escape single quotes by doubling them
71+
end
72+
73+
# @override
74+
# @note Implements AbstractAdapter interface method
75+
# @param [String] table Table name (unused)
76+
# @param [String] attr Attribute name
77+
# @return [String] Quoted column name
78+
def quote_table_name_for_assignment(table, attr)
79+
quote_column_name(attr)
80+
end
81+
82+
# @override
83+
# @note Implements AbstractAdapter interface method
84+
# @param [Time] value Time value to quote
85+
# @return [String] Quoted time string
86+
def quoted_time(value)
87+
value = value.change(year: 2000, month: 1, day: 1)
88+
quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
89+
end
90+
91+
# @override
92+
# @note Implements AbstractAdapter interface method
93+
# @param [String] value Binary value to quote
94+
# @return [String] Quoted binary string in hex format
95+
def quoted_binary(value)
96+
"x'#{value.hex}'"
97+
end
98+
99+
# @override
100+
# @note Implements AbstractAdapter interface method
101+
# @return [String] Quoted true value for DuckDB
102+
def quoted_true
103+
"1"
104+
end
105+
106+
# @override
107+
# @note Implements AbstractAdapter interface method
108+
# @return [Integer] Unquoted true value for DuckDB
109+
def unquoted_true
110+
1
111+
end
112+
113+
# @override
114+
# @note Implements AbstractAdapter interface method
115+
# @return [String] Quoted false value for DuckDB
116+
def quoted_false
117+
"0"
118+
end
119+
120+
# @override
121+
# @note Implements AbstractAdapter interface method
122+
# @return [Integer] Unquoted false value for DuckDB
123+
def unquoted_false
124+
0
125+
end
126+
127+
# @override
128+
# @note Implements AbstractAdapter interface method
129+
# @param [Object] value Default value to quote
130+
# @param [ActiveRecord::ConnectionAdapters::Column] column Column object
131+
# @return [String] Quoted default expression
132+
def quote_default_expression(value, column) # :nodoc:
133+
if value.is_a?(Proc)
134+
value = value.call
135+
# Don't wrap nextval() calls in extra parentheses
136+
value
137+
elsif value.is_a?(String) && value.match?(/\Anextval\(/i)
138+
# Handle nextval function calls for sequences - don't quote them
139+
value
140+
else
141+
super
142+
end
143+
end
144+
145+
# @override
146+
# @note Implements AbstractAdapter interface method
147+
# @param [Object] value Value to type cast
148+
# @return [Object] Type-cast value
149+
def type_cast(value) # :nodoc:
150+
case value
151+
when BigDecimal, Rational
152+
value.to_f
153+
when String
154+
if value.encoding == Encoding::ASCII_8BIT
155+
super(value.encode(Encoding::UTF_8))
156+
else
157+
super
158+
end
159+
else
160+
super
161+
end
162+
end
163+
end
164+
end
165+
end
166+
end

0 commit comments

Comments
 (0)