tina4ruby 3.13.85 → 3.13.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tina4/frond.rb CHANGED
@@ -106,7 +106,11 @@ module Tina4
106
106
  " and ", " or ", " not ",
107
107
  " + ", " - ", " * ", " // ", " / ", " % ", " ** "
108
108
  ].freeze
109
- FUNC_CALL_RE = /\A(\w+)\s*\((.*)\)\z/m
109
+ # A dot is allowed in the callee so {% import "f" as m %} can register its macros
110
+ # under the literal key "m.greet" and {{ m.greet("Andre") }} resolves as a call.
111
+ # Without the dot the whole expression was not recognised as a function call at
112
+ # all, so an aliased macro rendered as SILENTLY EMPTY.
113
+ FUNC_CALL_RE = /\A([\w.]+)\s*\((.*)\)\z/m
110
114
  FILTER_WITH_ARGS_RE = /\A(\w+)\s*\((.*)\)\z/m
111
115
  FILTER_CMP_RE = /\A(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)\z/
112
116
  OR_SPLIT_RE = /\s+or\s+/
@@ -136,6 +140,7 @@ module Tina4
136
140
  .gsub("<", "&lt;").gsub(">", "&gt;")
137
141
  end
138
142
  FROM_IMPORT_RE = /\Afrom\s+["'](.+?)["']\s+import\s+(.+)/
143
+ IMPORT_AS_RE = /\Aimport\s+["'](.+?)["']\s+as\s+(\w+)/
139
144
  CACHE_RE = /\Acache\s+["'](.+?)["']\s*(\d+)?/
140
145
  SPACELESS_RE = />\s+</
141
146
  AUTOESCAPE_RE = /\Aautoescape\s+(false|true)/
@@ -147,6 +152,17 @@ module Tina4
147
152
  # Set of common no-arg filter names that can be inlined for speed
148
153
  INLINE_FILTERS = %w[upper lower length trim capitalize title string int escape e].each_with_object({}) { |f, h| h[f] = true }.freeze
149
154
 
155
+ # Hard cap on the template caches — @compiled and @compiled_strings
156
+ # (ADR-0004, parity with PHP/Python/Node TEMPLATE_CACHE_MAX).
157
+ #
158
+ # An entry here is a whole token list, so the cap sits well below what a
159
+ # per-expression memo would justify. 256 is far above any real
160
+ # application's template count, so a normal app never evicts. The cap
161
+ # exists for the workload that genuinely grows without limit for the life
162
+ # of a worker: +render_string+ keys on md5(source), so an app that builds
163
+ # template strings dynamically adds an entry per distinct string.
164
+ TEMPLATE_CACHE_MAX = 256
165
+
150
166
  # -- Lazy context overlay for for-loops (avoids full Hash#dup) --
151
167
  class LoopContext
152
168
  def initialize(parent)
@@ -302,6 +318,7 @@ module Tina4
302
318
  source = File.read(path, encoding: "utf-8")
303
319
  mtime = File.mtime(path)
304
320
  tokens = tokenize(source)
321
+ cap_cache(@compiled, TEMPLATE_CACHE_MAX)
305
322
  @compiled[template] = [tokens, mtime, Time.now.to_i]
306
323
  execute_with_tokens(source, tokens, context)
307
324
  end
@@ -318,6 +335,7 @@ module Tina4
318
335
  end
319
336
 
320
337
  tokens = tokenize(source)
338
+ cap_cache(@compiled_strings, TEMPLATE_CACHE_MAX)
321
339
  @compiled_strings[key] = tokens
322
340
  execute_cached(tokens, context)
323
341
  end
@@ -413,6 +431,27 @@ module Tina4
413
431
 
414
432
  private
415
433
 
434
+ # Keep a memo cache bounded (ADR-0004). Call immediately before inserting
435
+ # a new entry.
436
+ #
437
+ # Eviction is insertion-ordered (oldest first), not true LRU: a Ruby Hash
438
+ # preserves insertion order, so dropping from the front is cheap, whereas
439
+ # refreshing recency on every cache HIT would add writes to the hottest
440
+ # path in a render and cost more than it saves. Half the cache is dropped
441
+ # at once so the sweep amortises to O(1) per insert.
442
+ #
443
+ # Evicting can never change what a render produces: every read site treats
444
+ # a miss as "recompute", so a swept entry is rebuilt on next use.
445
+ #
446
+ # @param cache [Hash] memo cache to bound, mutated in place
447
+ # @param max_entries [Integer] cap for this cache
448
+ # @return [void]
449
+ def cap_cache(cache, max_entries)
450
+ return if cache.size < max_entries
451
+
452
+ cache.keys.first(max_entries / 2).each { |key| cache.delete(key) }
453
+ end
454
+
416
455
  # -----------------------------------------------------------------------
417
456
  # Tokenizer
418
457
  # -----------------------------------------------------------------------
@@ -625,6 +664,9 @@ module Tina4
625
664
  end
626
665
  when "macro"
627
666
  i = handle_macro(tokens, i, context)
667
+ when "import"
668
+ handle_import_as(content, context)
669
+ i += 1
628
670
  when "from"
629
671
  handle_from_import(content, context)
630
672
  i += 1
@@ -837,12 +879,22 @@ module Tina4
837
879
  # Find the first occurrence of +needle+ that is not inside quotes or
838
880
  # parentheses. Returns the index, or -1 if not found.
839
881
  def find_outside_quotes(expr, needle)
882
+ # Fast path. This is the hottest method in a render -- profiling the Python
883
+ # twin showed 415,200 calls and 53% of render time for a single 20-row loop
884
+ # template, and the overwhelming majority return -1 because the needle
885
+ # simply is not in the expression. include? is a C-level scan, so bailing
886
+ # here skips the whole Ruby character loop. Exact, not a heuristic: a
887
+ # needle absent from the string cannot be present outside quotes either.
888
+ return -1 unless expr.include?(needle)
889
+
840
890
  in_q = nil
841
891
  depth = 0
842
892
  bracket_depth = 0
843
893
  i = 0
844
894
  nlen = needle.length
845
- while i <= expr.length - nlen
895
+ # Hoisted: expr.length was recomputed on every iteration of the condition.
896
+ last_start = expr.length - nlen
897
+ while i <= last_start
846
898
  ch = expr[i]
847
899
  if (ch == '"' || ch == "'") && depth == 0
848
900
  if in_q.nil?
@@ -1072,44 +1124,111 @@ module Tina4
1072
1124
  # Helpers return :not_matched when the expression doesn't match their
1073
1125
  # type, so the dispatcher falls through to the next handler.
1074
1126
 
1127
+ # Bound for the expression-form cache. Python bounds its expression caches
1128
+ # (lru_cache 1024) and PHP's were unbounded instance arrays until ADR-0004;
1129
+ # a template with generated expression strings must not grow this forever.
1130
+ # FIFO drop-oldest-half, deliberately not LRU (no per-hit bookkeeping on the
1131
+ # hot path).
1132
+ EXPR_FORM_CACHE_MAX = 2048
1133
+
1134
+ # ── Expression evaluation: a cascade, with the branch memoised ──
1135
+ #
1136
+ # eval_expr tries 12 structural forms in Twig precedence order. The catch is
1137
+ # that the LAST one, `resolve`, is by far the most common (a plain `i.name`),
1138
+ # so every ordinary variable used to walk all 11 detectors ahead of it on
1139
+ # EVERY render. Measured: ~6-7 microseconds and ~38 Frond method calls per
1140
+ # expression evaluation, linear in expression count.
1141
+ #
1142
+ # So: on first sight run the real cascade and RECORD which branch fired; on
1143
+ # later renders jump straight to that branch. The cache key is the expression
1144
+ # string, and a form is a pure property of that string.
1145
+ #
1146
+ # Correctness is owned by the existing evaluators, not by a second copy of the
1147
+ # detection logic (which is what would drift). The fast path calls exactly one
1148
+ # evaluator; if it declines -- returns its own :not_* sentinel -- we fall
1149
+ # through to the full cascade and re-record. A wrong cache entry therefore
1150
+ # costs one wasted call, never a wrong render.
1075
1151
  def eval_expr(expr, context)
1076
1152
  expr = expr.strip
1077
1153
  return nil if expr.empty?
1078
1154
 
1155
+ form = (@expr_form ||= {})[expr]
1156
+ if form
1157
+ result = eval_expr_as(form, expr, context)
1158
+ return result unless result == :form_declined
1159
+ end
1160
+
1079
1161
  result = eval_literal(expr)
1080
- return result unless result == :not_literal
1162
+ return remember_form(expr, :literal, result) unless result == :not_literal
1081
1163
 
1082
1164
  result = eval_collection_literal(expr, context)
1083
- return result unless result == :not_collection
1165
+ return remember_form(expr, :collection, result) unless result == :not_collection
1084
1166
 
1085
- return eval_expr(expr[1..-2], context) if matched_parens?(expr)
1167
+ if matched_parens?(expr)
1168
+ remember_form(expr, :parens, nil)
1169
+ return eval_expr(expr[1..-2], context)
1170
+ end
1086
1171
 
1087
1172
  result = eval_ternary(expr, context)
1088
- return result unless result == :not_ternary
1173
+ return remember_form(expr, :ternary, result) unless result == :not_ternary
1089
1174
 
1090
1175
  result = eval_inline_if(expr, context)
1091
- return result unless result == :not_inline_if
1176
+ return remember_form(expr, :inline_if, result) unless result == :not_inline_if
1092
1177
 
1093
1178
  result = eval_null_coalesce(expr, context)
1094
- return result unless result == :not_coalesce
1179
+ return remember_form(expr, :coalesce, result) unless result == :not_coalesce
1095
1180
 
1096
1181
  result = eval_concat(expr, context)
1097
- return result unless result == :not_concat
1182
+ return remember_form(expr, :concat, result) unless result == :not_concat
1098
1183
 
1099
- return eval_comparison(expr, context) if has_comparison?(expr)
1184
+ if has_comparison?(expr)
1185
+ remember_form(expr, :comparison, nil)
1186
+ return eval_comparison(expr, context)
1187
+ end
1100
1188
 
1101
1189
  result = eval_arithmetic(expr, context)
1102
- return result unless result == :not_arithmetic
1190
+ return remember_form(expr, :arithmetic, result) unless result == :not_arithmetic
1103
1191
 
1104
1192
  result = eval_filter_pipe(expr, context)
1105
- return result unless result == :not_filter_pipe
1193
+ return remember_form(expr, :pipe, result) unless result == :not_filter_pipe
1106
1194
 
1107
1195
  result = eval_function_call(expr, context)
1108
- return result unless result == :not_function
1196
+ return remember_form(expr, :function, result) unless result == :not_function
1109
1197
 
1198
+ remember_form(expr, :resolve, nil)
1110
1199
  resolve(expr, context)
1111
1200
  end
1112
1201
 
1202
+ # Record the branch an expression took, then hand back its value unchanged.
1203
+ def remember_form(expr, form, result)
1204
+ cache = (@expr_form ||= {})
1205
+ if cache.length >= EXPR_FORM_CACHE_MAX
1206
+ cache.keys.first(EXPR_FORM_CACHE_MAX / 2).each { |k| cache.delete(k) }
1207
+ end
1208
+ cache[expr] = form
1209
+ result
1210
+ end
1211
+
1212
+ # Evaluate via the one remembered branch. Returns :form_declined when that
1213
+ # branch no longer applies, so the caller re-runs the full cascade.
1214
+ def eval_expr_as(form, expr, context)
1215
+ case form
1216
+ when :resolve then resolve(expr, context)
1217
+ when :pipe then (r = eval_filter_pipe(expr, context)) == :not_filter_pipe ? :form_declined : r
1218
+ when :literal then (r = eval_literal(expr)) == :not_literal ? :form_declined : r
1219
+ when :function then (r = eval_function_call(expr, context)) == :not_function ? :form_declined : r
1220
+ when :concat then (r = eval_concat(expr, context)) == :not_concat ? :form_declined : r
1221
+ when :comparison then has_comparison?(expr) ? eval_comparison(expr, context) : :form_declined
1222
+ when :arithmetic then (r = eval_arithmetic(expr, context)) == :not_arithmetic ? :form_declined : r
1223
+ when :ternary then (r = eval_ternary(expr, context)) == :not_ternary ? :form_declined : r
1224
+ when :inline_if then (r = eval_inline_if(expr, context)) == :not_inline_if ? :form_declined : r
1225
+ when :coalesce then (r = eval_null_coalesce(expr, context)) == :not_coalesce ? :form_declined : r
1226
+ when :collection then (r = eval_collection_literal(expr, context)) == :not_collection ? :form_declined : r
1227
+ when :parens then matched_parens?(expr) ? eval_expr(expr[1..-2], context) : :form_declined
1228
+ else :form_declined
1229
+ end
1230
+ end
1231
+
1113
1232
  # ── Filter pipe: value|filter(args) ──
1114
1233
  # Reached only after every looser-binding operator (concat ~, comparison,
1115
1234
  # arithmetic, ternary, ...) has been ruled out at this level, so the pipe
@@ -1129,6 +1248,10 @@ module Tina4
1129
1248
  # looser than the filter pipe. Quote/paren-aware via find_outside_quotes so
1130
1249
  # operator-like text inside a string literal or filter args never matches.
1131
1250
  def has_looser_than_pipe_operator?(expr)
1251
+ # Leading unary `not` -- see has_comparison? for why a space-delimited
1252
+ # match cannot see it. `not x|upper` must be `not (x|upper)`.
1253
+ return true if expr.start_with?("not ")
1254
+
1132
1255
  LOOSER_THAN_PIPE_OPS.any? { |op| find_outside_quotes(expr, op) >= 0 }
1133
1256
  end
1134
1257
 
@@ -1244,7 +1367,12 @@ module Tina4
1244
1367
  def eval_concat(expr, context)
1245
1368
  return :not_concat unless expr.include?("~")
1246
1369
  parts = expr.split("~")
1247
- parts.map { |p| (eval_expr(p.strip, context) || "").to_s }.join
1370
+ # `|| ""` would swallow a legitimate `false`: in Ruby only nil and false are
1371
+ # falsy, so `false || ""` is "" and a boolean rendered as EMPTY. Guard on nil
1372
+ # only -- false must render as "false", matching the other three frameworks
1373
+ # (and line 635, which already got this right, which is why a comparison
1374
+ # rendered "false" while a bare false variable rendered "").
1375
+ parts.map { |p| v = eval_expr(p.strip, context); v.nil? ? "" : v.to_s }.join
1248
1376
  end
1249
1377
 
1250
1378
  # ── Arithmetic: +, -, *, //, /, %, ** ──
@@ -1272,7 +1400,21 @@ module Tina4
1272
1400
  fn.call(*args)
1273
1401
  end
1274
1402
 
1403
+ # True when the expression carries a comparison/logical operator, so it is
1404
+ # routed to eval_comparison -- the SAME evaluator {% if %} uses. A condition
1405
+ # therefore means the same thing in a condition and in an output expression.
1406
+ #
1407
+ # The LEADING unary `not` needs its own check: every operator in the list is
1408
+ # matched WITH surrounding spaces, so `not x` (nothing to its left) matched
1409
+ # none of them and fell through to the variable-resolution tail, which
1410
+ # looked up a variable literally named "not x", found nothing, and rendered
1411
+ # EMPTY. `{% if not x %}` and `x and not y` always worked; only the
1412
+ # standalone `{{ not x }}` was dropped, and before booleans rendered
1413
+ # lowercase a dropped expression and `false -> ''` looked identical.
1414
+ # Fixed in 3.13.87 alongside the boolean contract.
1275
1415
  def has_comparison?(expr)
1416
+ return true if expr.start_with?("not ")
1417
+
1276
1418
  [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<",
1277
1419
  " and ", " or ", " not "].any? { |op| expr.include?(op) }
1278
1420
  end
@@ -1438,7 +1580,19 @@ module Tina4
1438
1580
  parts.each do |part|
1439
1581
  part = part.strip.gsub(RESOLVE_STRIP_RE, "") # strip quotes from bracket access
1440
1582
  if value.is_a?(Hash) || value.is_a?(LoopContext)
1441
- value = value[part] || value[part.to_sym]
1583
+ # `a[k] || a[k.to_sym]` LOSES a stored `false`: only nil and false are
1584
+ # falsy in Ruby, so a legitimate false fell through to the symbol lookup,
1585
+ # missed, and became nil -- rendering as EMPTY. That is why
1586
+ # `{{ flag }}` printed nothing while `{{ n < 3 }}` printed "false".
1587
+ # Probe the string key by presence, not truthiness.
1588
+ value = if value.is_a?(Hash) && value.key?(part)
1589
+ value[part]
1590
+ elsif value.is_a?(Hash) && value.key?(part.to_sym)
1591
+ value[part.to_sym]
1592
+ else
1593
+ sv = value[part]
1594
+ sv.nil? ? value[part.to_sym] : sv
1595
+ end
1442
1596
  elsif value.is_a?(Array)
1443
1597
  # Slice syntax: value[1:5], value[:10], value[start:end]
1444
1598
  if part.include?(":") && !(part.start_with?('"') || part.start_with?("'"))
@@ -1740,7 +1894,7 @@ module Tina4
1740
1894
  end
1741
1895
 
1742
1896
  macro_name = m[1]
1743
- param_names = m[2].split(",").map(&:strip).reject(&:empty?)
1897
+ params = parse_macro_params(m[2])
1744
1898
 
1745
1899
  body_tokens = []
1746
1900
  i = start + 1
@@ -1759,8 +1913,8 @@ module Tina4
1759
1913
 
1760
1914
  context[macro_name] = lambda { |*args|
1761
1915
  macro_ctx = captured_context.dup
1762
- param_names.each_with_index do |pname, pi|
1763
- macro_ctx[pname] = pi < args.length ? args[pi] : nil
1916
+ params.each_with_index do |(pname, pdefault), pi|
1917
+ macro_ctx[pname] = pi < args.length ? args[pi] : pdefault
1764
1918
  end
1765
1919
  Tina4::SafeString.new(engine.send(:render_tokens, captured_body.dup, macro_ctx))
1766
1920
  }
@@ -1768,6 +1922,54 @@ module Tina4
1768
1922
  i
1769
1923
  end
1770
1924
 
1925
+ # {% import "file" as alias %} — load EVERY macro in a file under one namespace.
1926
+ #
1927
+ # Macros are registered in the context under the dotted key "alias.name", so
1928
+ # {{ alias.greet("Andre") }} resolves through the ordinary function-call path
1929
+ # (FUNC_CALL_RE now admits a dot) and reuses _make_macro_fn — identical argument
1930
+ # binding, default handling and SafeString output as every other macro. Both
1931
+ # import forms therefore render identically, the contract Python locks too.
1932
+ def handle_import_as(content, context)
1933
+ m = content.match(IMPORT_AS_RE)
1934
+ return unless m
1935
+
1936
+ filename = m[1]
1937
+ alias_name = m[2]
1938
+ source = load_template(filename)
1939
+ tokens = tokenize(source)
1940
+
1941
+ i = 0
1942
+ while i < tokens.length
1943
+ ttype, raw = tokens[i]
1944
+ if ttype == BLOCK
1945
+ tag_content, = strip_tag(raw)
1946
+ if tag_content.split(/\s+/).first == "macro"
1947
+ macro_m = tag_content.match(MACRO_RE)
1948
+ if macro_m
1949
+ macro_name = macro_m[1]
1950
+ params = parse_macro_params(macro_m[2])
1951
+
1952
+ body_tokens = []
1953
+ i += 1
1954
+ while i < tokens.length
1955
+ if tokens[i][0] == BLOCK && tokens[i][1].include?("endmacro")
1956
+ i += 1
1957
+ break
1958
+ end
1959
+ body_tokens << tokens[i]
1960
+ i += 1
1961
+ end
1962
+
1963
+ context["#{alias_name}.#{macro_name}"] =
1964
+ _make_macro_fn(body_tokens.dup, params.dup, context.dup)
1965
+ next
1966
+ end
1967
+ end
1968
+ end
1969
+ i += 1
1970
+ end
1971
+ end
1972
+
1771
1973
  # {% from "file" import macro1, macro2 %}
1772
1974
  def handle_from_import(content, context)
1773
1975
  m = content.match(FROM_IMPORT_RE)
@@ -1789,7 +1991,7 @@ module Tina4
1789
1991
  macro_m = tag_content.match(MACRO_RE)
1790
1992
  if macro_m && names.include?(macro_m[1])
1791
1993
  macro_name = macro_m[1]
1792
- param_names = macro_m[2].split(",").map(&:strip).reject(&:empty?)
1994
+ param_names = parse_macro_params(macro_m[2])
1793
1995
 
1794
1996
  body_tokens = []
1795
1997
  i += 1
@@ -1811,13 +2013,34 @@ module Tina4
1811
2013
  end
1812
2014
  end
1813
2015
 
2016
+ # Parse a macro parameter list into [name, default] pairs.
2017
+ #
2018
+ # Handles: name, name="default", name='default'. Splitting on "," alone left
2019
+ # a defaulted parameter NAMED "greeting='Hello'", so the body's {{ greeting }}
2020
+ # matched nothing (rendered empty) AND the caller's positional argument was
2021
+ # stored under that junk key and lost. Mirrors the Python master's
2022
+ # _parse_macro_params. `default` is nil when none is declared.
2023
+ def parse_macro_params(raw_params)
2024
+ raw_params.split(",").map(&:strip).reject(&:empty?).map do |p|
2025
+ name, default = p.split("=", 2)
2026
+ default = default.strip if default
2027
+ if default && default.length >= 2 &&
2028
+ ((default.start_with?('"') && default.end_with?('"')) ||
2029
+ (default.start_with?("'") && default.end_with?("'")))
2030
+ default = default[1..-2]
2031
+ end
2032
+ [name.strip, default]
2033
+ end
2034
+ end
2035
+
1814
2036
  # Build an isolated lambda for a macro — avoids closure-in-loop variable sharing.
1815
- def _make_macro_fn(body_tokens, param_names, ctx)
2037
+ # `params` is the [name, default] list from parse_macro_params.
2038
+ def _make_macro_fn(body_tokens, params, ctx)
1816
2039
  engine = self
1817
2040
  lambda { |*args|
1818
2041
  macro_ctx = ctx.dup
1819
- param_names.each_with_index do |pname, pi|
1820
- macro_ctx[pname] = pi < args.length ? args[pi] : nil
2042
+ params.each_with_index do |(pname, pdefault), pi|
2043
+ macro_ctx[pname] = pi < args.length ? args[pi] : pdefault
1821
2044
  end
1822
2045
  Tina4::SafeString.new(engine.send(:render_tokens, body_tokens.dup, macro_ctx))
1823
2046
  }
data/lib/tina4/metrics.rb CHANGED
@@ -338,7 +338,14 @@ module Tina4
338
338
  "total_functions" => all_functions.length,
339
339
  "avg_complexity" => avg_cc.round(2),
340
340
  "avg_maintainability" => avg_mi.round(1),
341
+ # Display-only: the top-15 for the "most complex functions" report.
342
+ # Do NOT source offenders / --fail-on from this — capping here silently
343
+ # hides the 16th+ over-threshold function from the gate. offenders()
344
+ # reads "all_functions" (below) instead.
341
345
  "most_complex_functions" => all_functions.first(15),
346
+ # Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
347
+ # so no function over the complexity threshold ever escapes the gate.
348
+ "all_functions" => all_functions,
342
349
  "file_metrics" => file_metrics,
343
350
  "violations" => violations,
344
351
  "dependency_graph" => import_graph,
@@ -386,8 +393,10 @@ module Tina4
386
393
 
387
394
  items = []
388
395
 
389
- # Function-level: cyclomatic complexity.
390
- (analysis["most_complex_functions"] || []).each do |fn|
396
+ # Function-level: cyclomatic complexity. Use the FULL function list (not the
397
+ # display-capped most_complex_functions[:15]) so a 16th+ over-threshold
398
+ # function is never silently dropped from the offenders list or --fail-on.
399
+ (analysis["all_functions"] || analysis["most_complex_functions"] || []).each do |fn|
391
400
  cc = fn["complexity"]
392
401
  next unless cc > 10
393
402
  items << {
data/lib/tina4/orm.rb CHANGED
@@ -785,9 +785,9 @@ module Tina4
785
785
  # Only adopt the engine-assigned id for an auto-increment PK. A
786
786
  # natural-key PK was set by the caller; don't overwrite it with the
787
787
  # driver's last_insert_id (which may be a sequence value that
788
- # doesn't apply here).
789
- if auto_increment && result[:last_id] && respond_to?("#{pk}=")
790
- __send__("#{pk}=", result[:last_id])
788
+ # doesn't apply here). db.insert returns a DatabaseResult (.last_id).
789
+ if auto_increment && result.last_id && respond_to?("#{pk}=")
790
+ __send__("#{pk}=", result.last_id)
791
791
  end
792
792
  end
793
793
  end