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- NOW_TOKEN =
Sql.Mutation.timestamped emits “$JADE_SQL_NOW$” where created_at / updated_at go. Swap it for one UTC timestamp literal per statement, computed here — the app clock, so it moves with travel_to/Timecop (unlike DB now()). The token lives in SQL we generate, never in a bound value, so it can’t collide with user data.
"$JADE_SQL_NOW$"- 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
-
.constraint_name(error) ⇒ Object
The constraint/index name behind a RecordNotUnique, so callers can route by which unique index was violated.
-
.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
- .fill_now(sql) ⇒ 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
207 208 209 210 211 212 |
# File 'lib/jade-sql/runtime.rb', line 207 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
233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/jade-sql/runtime.rb', line 233 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.
87 88 89 |
# File 'lib/jade-sql/runtime.rb', line 87 def self.coerce_row(row) row.transform_values { |v| coerce_value(v) } end |
.coerce_value(v) ⇒ Object
91 92 93 94 95 96 97 98 99 100 |
# File 'lib/jade-sql/runtime.rb', line 91 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 |
.constraint_name(error) ⇒ Object
The constraint/index name behind a RecordNotUnique, so callers can route by which unique index was violated. PG reports it in the error’s diagnostics; other adapters (or a missing name) fall back to “”.
172 173 174 175 176 177 178 179 |
# File 'lib/jade-sql/runtime.rb', line 172 def self.constraint_name(error) cause = error.cause return "" unless defined?(::PG::Result) && cause.respond_to?(:result) && cause.result cause.result.error_field(::PG::Result::PG_DIAG_CONSTRAINT_NAME) || "" rescue StandardError "" 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.
108 109 110 111 112 113 114 115 |
# File 'lib/jade-sql/runtime.rb', line 108 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
165 166 167 |
# File 'lib/jade-sql/runtime.rb', line 165 def self.decode_element(raw) raw == "NULL" ? nil : raw end |
.fill_now(sql) ⇒ Object
188 189 190 191 192 193 |
# File 'lib/jade-sql/runtime.rb', line 188 def self.fill_now(sql) return sql unless sql.include?(NOW_TOKEN) stamp = "'#{::Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%6N+00')}'" sql.gsub(NOW_TOKEN) { stamp } end |
.parse_pg_array(s) ⇒ Object
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 158 159 160 161 162 163 |
# File 'lib/jade-sql/runtime.rb', line 129 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
125 126 127 |
# File 'lib/jade-sql/runtime.rb', line 125 def self.pg_array_literal?(s) s.match?(PG_ARRAY_LITERAL) && !s.match?(JSON_OBJECT_HEAD) end |
.typed_param(value) ⇒ Object
224 225 226 227 228 229 230 231 |
# File 'lib/jade-sql/runtime.rb', line 224 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.
218 219 220 221 222 |
# File 'lib/jade-sql/runtime.rb', line 218 def self.typed_params(params, conn) return params unless conn.adapter_name =~ /postgres/i params.map { |p| typed_param(p) } end |