Module: JadeSql::Runtime
- Extended by:
- Jade::Port
- Defined in:
- lib/jade-sql/runtime.rb
Constant Summary collapse
- PG_ARRAY_LITERAL =
PG arrays render as ‘{}`, `a,b,c`, `“a,b”,“c”`, with NULL as bare `NULL`. Quoted elements escape `“` and `` with backslashes. The JSON-object guard rejects `”key“:…` shapes — they share the outer braces but should reach Decode.Value as plain strings (or as Hash if AR already typecast the column).
/\A\{.*\}\z/m- JSON_OBJECT_HEAD =
/\A\{\s*"[^"]*"\s*:/m- QUOTED_OR_PLACEHOLDER =
Sql renders ‘?` placeholders uniformly. AR’s exec_query/exec_update path on the PG adapter expects ‘$1, $2, …` — there is no `?`-to-`$n` rewrite at that layer. SQLite and MySQL accept `?` directly, so this is a no-op there.
The alternation matches a whole quoted span first (single-quoted string with ‘”` escapes, or double-quoted identifier with `“”` escapes), so a literal `?` inside one is left alone — only bare `?` outside quotes becomes a placeholder. Dollar-quoted bodies aren’t handled (uncommon in app SQL).
/'(?:[^']|'')*'|"(?:[^"]|"")*"|\?/
Class Method Summary collapse
- .adapt_sql(sql, conn) ⇒ Object
- .array_type_for(elements) ⇒ Object
-
.coerce_row(row) ⇒ Object
AR’s PG adapter returns ::Date / ::Time for date/timestamp columns; Calendar.Date / Clock.Instant decoders expect ISO strings.
- .coerce_value(v) ⇒ Object
-
.decimal_wire(v) ⇒ Object
::BigDecimal -> “<mantissa>e<exponent>” with value = mantissa * 10^exp, exactly (BigDecimal#split gives sign, significant digits, and a base-10 exponent).
- .decode_element(raw) ⇒ Object
- .parse_pg_array(s) ⇒ Object
- .pg_array_literal?(s) ⇒ Boolean
- .typed_param(value) ⇒ Object
-
.typed_params(params, conn) ⇒ Object
AR’s exec_query raw path can’t bind a Ruby Array — pg’s OID type cast isn’t applied to bare values.
Class Method Details
.adapt_sql(sql, conn) ⇒ Object
175 176 177 178 179 180 |
# File 'lib/jade-sql/runtime.rb', line 175 def self.adapt_sql(sql, conn) return sql unless conn.adapter_name =~ /postgres/i n = 0 sql.gsub(QUOTED_OR_PLACEHOLDER) { |m| m == "?" ? "$#{n += 1}" : m } end |
.array_type_for(elements) ⇒ Object
201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/jade-sql/runtime.rb', line 201 def self.array_type_for(elements) sample = elements.find { |e| !e.nil? } element_type = case sample when ::Integer then ::ActiveRecord::Type::Integer.new when ::Float then ::ActiveRecord::Type::Float.new when true, false then ::ActiveRecord::Type::Boolean.new else ::ActiveRecord::Type::String.new end ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(element_type) end |
.coerce_row(row) ⇒ Object
AR’s PG adapter returns ::Date / ::Time for date/timestamp columns; Calendar.Date / Clock.Instant decoders expect ISO strings. Coerce at the boundary so callers don’t sprinkle text_cast in every SELECT.
text[] / int[] / etc. arrive as Postgres array literals (‘a,b,c`) when AR’s exec_query path doesn’t run the OID typecast. Parse them back to Ruby Arrays so ‘Decode.list(…)` works the same as for any other List(a) column.
numeric/decimal columns come back as ::BigDecimal; the schema generator maps them to Sql.Decimal, whose decoder reads the exact “<mantissa>e<exponent>” wire form. Float would lose precision, so don’t.
81 82 83 |
# File 'lib/jade-sql/runtime.rb', line 81 def self.coerce_row(row) row.transform_values { |v| coerce_value(v) } end |
.coerce_value(v) ⇒ Object
85 86 87 88 89 90 91 92 93 94 |
# File 'lib/jade-sql/runtime.rb', line 85 def self.coerce_value(v) case v when ::Date then v.iso8601 when ::Time, ::DateTime then v.iso8601 when ::BigDecimal then decimal_wire(v) when ::String pg_array_literal?(v) ? parse_pg_array(v) : v else v end end |
.decimal_wire(v) ⇒ Object
::BigDecimal -> “<mantissa>e<exponent>” with value = mantissa * 10^exp, exactly (BigDecimal#split gives sign, significant digits, and a base-10 exponent). Matches the wire form Sql.Decimal’s decoder parses.
‘NaN’/‘Infinity’::numeric are legal Postgres values that Sql.Decimal can’t represent; fail loudly rather than silently decode them as 0.
102 103 104 105 106 107 108 109 |
# File 'lib/jade-sql/runtime.rb', line 102 def self.decimal_wire(v) raise ArgumentError, "non-finite numeric: #{v}" unless v.finite? sign, digits, _base, exp = v.split mantissa = sign * digits.to_i exponent = exp - digits.length "#{mantissa}e#{exponent}" end |
.decode_element(raw) ⇒ Object
159 160 161 |
# File 'lib/jade-sql/runtime.rb', line 159 def self.decode_element(raw) raw == "NULL" ? nil : raw end |
.parse_pg_array(s) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/jade-sql/runtime.rb', line 123 def self.parse_pg_array(s) inner = s[1..-2] return [] if inner.empty? elements = [] buffer = String.new in_quotes = false i = 0 while i < inner.length c = inner[i] if in_quotes if c == '\\' && i + 1 < inner.length buffer << inner[i + 1] i += 2 next elsif c == '"' in_quotes = false else buffer << c end else if c == '"' in_quotes = true elsif c == ',' elements << decode_element(buffer) buffer = String.new else buffer << c end end i += 1 end elements << decode_element(buffer) elements end |
.pg_array_literal?(s) ⇒ Boolean
119 120 121 |
# File 'lib/jade-sql/runtime.rb', line 119 def self.pg_array_literal?(s) s.match?(PG_ARRAY_LITERAL) && !s.match?(JSON_OBJECT_HEAD) end |
.typed_param(value) ⇒ Object
192 193 194 195 196 197 198 199 |
# File 'lib/jade-sql/runtime.rb', line 192 def self.typed_param(value) case value when ::Array ::ActiveRecord::Relation::QueryAttribute.new(nil, value, array_type_for(value)) else value end end |
.typed_params(params, conn) ⇒ Object
AR’s exec_query raw path can’t bind a Ruby Array — pg’s OID type cast isn’t applied to bare values. Wrap arrays in QueryAttribute with a PG OID::Array so the binding picks the right wire format. Element type sniffs the first non-nil entry; falls back to text.
186 187 188 189 190 |
# File 'lib/jade-sql/runtime.rb', line 186 def self.typed_params(params, conn) return params unless conn.adapter_name =~ /postgres/i params.map { |p| typed_param(p) } end |