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
204 205 206 207 208 209 |
# File 'lib/jade-sql/runtime.rb', line 204 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
230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/jade-sql/runtime.rb', line 230 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
"
84 85 86 |
# File 'lib/jade-sql/runtime.rb', line 84 def self.coerce_row(row) row.transform_values { |v| coerce_value(v) } end |
.coerce_value(v) ⇒ Object
88 89 90 91 92 93 94 95 96 97 |
# File 'lib/jade-sql/runtime.rb', line 88 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 "".
169 170 171 172 173 174 175 176 |
# File 'lib/jade-sql/runtime.rb', line 169 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.
105 106 107 108 109 110 111 112 |
# File 'lib/jade-sql/runtime.rb', line 105 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
162 163 164 |
# File 'lib/jade-sql/runtime.rb', line 162 def self.decode_element(raw) raw == "NULL" ? nil : raw end |
.fill_now(sql) ⇒ Object
185 186 187 188 189 190 |
# File 'lib/jade-sql/runtime.rb', line 185 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
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 158 159 160 |
# File 'lib/jade-sql/runtime.rb', line 126 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
122 123 124 |
# File 'lib/jade-sql/runtime.rb', line 122 def self.pg_array_literal?(s) s.match?(PG_ARRAY_LITERAL) && !s.match?(JSON_OBJECT_HEAD) end |
.typed_param(value) ⇒ Object
221 222 223 224 225 226 227 228 |
# File 'lib/jade-sql/runtime.rb', line 221 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.
215 216 217 218 219 |
# File 'lib/jade-sql/runtime.rb', line 215 def self.typed_params(params, conn) return params unless conn.adapter_name =~ /postgres/i params.map { |p| typed_param(p) } end |