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 bareNULL. 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-$nrewrite 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 -> "
e " with value = coeff * 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
214 215 216 217 218 219 |
# File 'lib/jade-sql/runtime.rb', line 214 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
240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/jade-sql/runtime.rb', line 240 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 jade's stdlib Decimal, whose decoder reads the exact
"
87 88 89 90 91 92 93 94 95 96 |
# File 'lib/jade-sql/runtime.rb', line 87 def self.coerce_row(row) out = nil row.each_pair do |k, v| coerced = coerce_value(v) (out ||= row.dup)[k] = coerced unless coerced.equal?(v) end out || row end |
.coerce_value(v) ⇒ Object
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/jade-sql/runtime.rb', line 98 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 "".
179 180 181 182 183 184 185 186 |
# File 'lib/jade-sql/runtime.rb', line 179 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 -> "
'NaN'/'Infinity'::numeric are legal Postgres values that Decimal can't represent; fail loudly rather than silently decode them as 0.
115 116 117 118 119 120 121 122 |
# File 'lib/jade-sql/runtime.rb', line 115 def self.decimal_wire(v) raise ArgumentError, "non-finite numeric: #{v}" unless v.finite? sign, digits, _base, exp = v.split coefficient = sign * digits.to_i exponent = exp - digits.length "#{coefficient}e#{exponent}" end |
.decode_element(raw) ⇒ Object
172 173 174 |
# File 'lib/jade-sql/runtime.rb', line 172 def self.decode_element(raw) raw == "NULL" ? nil : raw end |
.fill_now(sql) ⇒ Object
195 196 197 198 199 200 |
# File 'lib/jade-sql/runtime.rb', line 195 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
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 164 165 166 167 168 169 170 |
# File 'lib/jade-sql/runtime.rb', line 136 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
132 133 134 |
# File 'lib/jade-sql/runtime.rb', line 132 def self.pg_array_literal?(s) s.match?(PG_ARRAY_LITERAL) && !s.match?(JSON_OBJECT_HEAD) end |
.typed_param(value) ⇒ Object
231 232 233 234 235 236 237 238 |
# File 'lib/jade-sql/runtime.rb', line 231 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.
225 226 227 228 229 |
# File 'lib/jade-sql/runtime.rb', line 225 def self.typed_params(params, conn) return params unless conn.adapter_name =~ /postgres/i params.map { |p| typed_param(p) } end |