From f6029b351eb3ec47fb844621573c44ce17880047 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 28 Aug 2026 23:07:10 -0700 Subject: [PATCH 1/2] fix(model,tools): BoxLang IN-list extraction parity and coverage-tool repairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model/sql.cfc: the BoxLang branch of $addWhereClauseParameters now matches the Lucee/Adobe ReplaceList path byte-for-byte — IN-lists keep their quoted items verbatim (the list branch of $queryParams unquotes via $cleanInStatementValue), and single values drop exactly one pair of outer quotes so inner apostrophes survive. Fixes crudSpec's 'parenthesis commas and single quotes' IN spec on every BoxLang leg (the previous regex-strip corrupted doubled-apostrophe values). - tools/code-quality/cfml-coverage.py: - mask strings and comments before matching so JS function literals inside CFML strings no longer get counters inserted mid-string (compile break in controller/channels.cfc) - capture only the cffunction NAME as the counter id (whole-tag ids embedded quotes = invalid CFML) - instrument guarded closure assignments (variables.$x = function) like the complexity analyzer does - collect all insertion points before mutating so counter insertions cannot shift later matches - combine accepts both the dict form (--baseline write) and the list form (--json) of the complexity input Signed-off-by: Peter Amiri --- tools/code-quality/cfml-coverage.py | 57 +++++++++++++++++++++++------ vendor/wheels/model/sql.cfc | 24 ++++++------ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/tools/code-quality/cfml-coverage.py b/tools/code-quality/cfml-coverage.py index 78d65882a..feacd79c2 100644 --- a/tools/code-quality/cfml-coverage.py +++ b/tools/code-quality/cfml-coverage.py @@ -22,9 +22,30 @@ import os, re, sys, json, shutil, argparse SCRIPT_FN = re.compile(r'\bfunction\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{', re.S) -TAG_FN = re.compile(r'(]*?\bname\s*=\s*["\']?[A-Za-z_$][\w$]*[^>]*?>)', re.I) +# Guarded closure assignments (include-idempotent templates): +# variables.$helper = function(args) { ... }; +CLOSURE_FN = re.compile(r'\bvariables\s*\.\s*\$?([A-Za-z_$][\w$]*)\s*=\s*function\s*\([^)]*\)\s*\{', re.S) +# Capture the name only (group 1); the opening tag itself is not part of the id — +# embedding it produced ids full of quotes, i.e. invalid CFML in the counter. +TAG_FN = re.compile(r']*?\bname\s*=\s*["\']?([A-Za-z_$][\w$]*)', re.I) EXCLUDE_DIRS = {'tests', 'rocketunit_tests'} +DSTRING = re.compile(r'"(?:[^"\\]|\\.)*"', re.S) +SSTRING = re.compile(r"'(?:[^'\\]|\\.)*'", re.S) +LINE_COMMENT = re.compile(r'//[^\n]*') +BLOCK_COMMENT = re.compile(r'/\*.*?\*/', re.S) +TAG_COMMENT = re.compile(r'', re.S) + + +def _mask(text): + """Mask strings and comments with same-length spaces so function patterns + never match inside them (JS `function name(){` in a CFML string literal + used to get a counter inserted mid-string — a parse error). Offsets are + preserved, so matches map back onto the original text unchanged.""" + for pat in (DSTRING, SSTRING, LINE_COMMENT, BLOCK_COMMENT, TAG_COMMENT): + text = pat.sub(lambda m: ' ' * len(m.group(0)), text) + return text + def _backup_dir(root): """Sibling of the instrumented root, so the walk never re-instruments it.""" @@ -59,17 +80,23 @@ def instrument(root): with open(path, encoding='utf-8', errors='replace') as fh: text = fh.read() rel = _rel(path, root) - # script functions: insert a script-syntax counter right after '{' + masked = _mask(text) + # Collect every insertion point against the original (masked) offsets + # first, then apply them right-to-left so earlier insertions never + # shift later positions. + matches = [] + for pat in (SCRIPT_FN, CLOSURE_FN): + for m in pat.finditer(masked): + matches.append((m.end(), 'script', m.group(1))) + for m in TAG_FN.finditer(masked): + brace = masked.find('>', m.end()) + if brace >= 0: + matches.append((brace + 1, 'tag', m.group(1))) + matches.sort(key=lambda t: -t[0]) out = text - for m in reversed(list(SCRIPT_FN.finditer(text))): - brace = m.end() # position just after '{' - ctr = SCRIPT_COUNTER.format(id=f"{rel}:{m.group(1)}") - out = out[:brace] + ctr + out[brace:] - n += 1 - # tag functions: insert a counter right after the opening tag - for m in reversed(list(TAG_FN.finditer(out))): - ctr = TAG_COUNTER.format(id=f"{rel}:{m.group(1)}") - out = out[:m.end()] + ctr + out[m.end():] + for pos, kind, name in matches: + ctr = (SCRIPT_COUNTER if kind == 'script' else TAG_COUNTER).format(id=f"{rel}:{name}") + out = out[:pos] + ctr + out[pos:] n += 1 if out != text: backup = os.path.join(_backup_dir(root), rel) @@ -104,7 +131,13 @@ def _cov_key(complexity_id): def combine(root, coverage_path, complexity_path): cov = json.load(open(coverage_path)) - complexity = {r['id']: r['complexity'] for r in json.load(open(complexity_path))} + raw_complexity = json.load(open(complexity_path)) + if isinstance(raw_complexity, dict): + # cfml-complexity.py --baseline write emits {id: complexity} + complexity = {rid: int(comp) for rid, comp in raw_complexity.items()} + else: + # --json emits a list of rows with id/complexity + complexity = {r['id']: int(r['complexity']) for r in raw_complexity} # cov keys are ":"; complexity ids are "::" rows = [] for cid, comp in complexity.items(): diff --git a/vendor/wheels/model/sql.cfc b/vendor/wheels/model/sql.cfc index 614a9afb2..9c93db000 100644 --- a/vendor/wheels/model/sql.cfc +++ b/vendor/wheels/model/sql.cfc @@ -1213,18 +1213,18 @@ component { } if (Find("'", local.processedValue) > 0 || Find(Chr(34), local.processedValue) > 0) { local.cleanedValue = local.processedValue; - // Unquote token delimiters only: fold quote-comma-quote - // separators into plain commas first (IN-lists), then strip - // one pair of outer quotes. The previous `'([^']*)'` - // regex-strip ate inner apostrophes — "O'Brien" came back - // as "OBrien" with a leaked trailing quote. - local.cleanedValue = ReReplace(local.cleanedValue, "'\s*,\s*'", ",", "ALL"); - local.doubleQuote = Chr(34); - local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#\s*,\s*#local.doubleQuote#", ",", "ALL"); - local.cleanedValue = ReReplace(local.cleanedValue, "^'", "", "ONE"); - local.cleanedValue = ReReplace(local.cleanedValue, "'$", "", "ONE"); - local.cleanedValue = ReReplace(local.cleanedValue, "^#local.doubleQuote#", "", "ONE"); - local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#$", "", "ONE"); + // IN-lists keep their quoted items verbatim (byte-identical + // to the Lucee/Adobe ReplaceList path — the list branch of + // $queryParams unquotes via $cleanInStatementValue). + // Single values drop exactly one pair of outer quotes so + // inner apostrophes survive ("O'Brien" — the previous + // regex-strip ate them and seedOnce re-created rows). + if (!(Find("','", local.cleanedValue) > 0 || Find("#Chr(34)#,#Chr(34)#", local.cleanedValue) > 0)) { + local.cleanedValue = ReReplace(local.cleanedValue, "^'", "", "ONE"); + local.cleanedValue = ReReplace(local.cleanedValue, "'$", "", "ONE"); + local.cleanedValue = ReReplace(local.cleanedValue, "^#Chr(34)#", "", "ONE"); + local.cleanedValue = ReReplace(local.cleanedValue, "#Chr(34)#$", "", "ONE"); + } ArrayAppend(local.originalValues, local.cleanedValue); } else { ArrayAppend(local.originalValues, local.processedValue); From b3849f1717ff593c25a4bd2666c19737c555b7b8 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 28 Aug 2026 23:25:06 -0700 Subject: [PATCH 2/2] fix(jobs): bound monitor and retry queries in SQL text, not driver maxrows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BoxLang's JDBC layer calls setLargeMaxRows(), which the PostgreSQL driver does not implement ('is not yet implemented') — getMonitorData's recentJobs/oldestPending queries threw, the surrounding catch returned empty arrays, and B3 failed on the BoxLang + PostgreSQL/CockroachDB legs. Bound all three queries with the existing dialect-aware $candidateLimitClause (LIMIT / FETCH FIRST / OFFSET-FETCH) instead of the driver maxrows option, with a CFML break as a backstop on the recent-jobs array build. Verified: jobs area 86/0/0 on BoxLang + PostgreSQL and BoxLang + SQLite. Signed-off-by: Peter Amiri --- vendor/wheels/JobWorker.cfc | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/vendor/wheels/JobWorker.cfc b/vendor/wheels/JobWorker.cfc index c924a74e0..64037c1fe 100644 --- a/vendor/wheels/JobWorker.cfc +++ b/vendor/wheels/JobWorker.cfc @@ -276,9 +276,18 @@ component { local.recentParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"}; } local.recentSql &= " ORDER BY updatedAt DESC"; - local.recentRows = queryExecute(local.recentSql, local.recentParams, {datasource = variables.$datasource, maxrows = 10}); + // Bound in SQL text via the dialect clause, not the driver maxrows + // option: BoxLang's JDBC layer calls setLargeMaxRows(), which the + // PostgreSQL driver does not implement ("is not yet implemented") — + // the query throws and the catch below returned an empty + // recentJobs. The CFML break is a belt-and-braces backstop. + local.recentSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = 10); + local.recentRows = queryExecute(local.recentSql, local.recentParams, {datasource = variables.$datasource}); for (local.row in local.recentRows) { + if (ArrayLen(local.result.recentJobs) >= 10) { + break; + } ArrayAppend(local.result.recentJobs, { id = local.row.id, jobClass = local.row.jobClass, @@ -302,7 +311,10 @@ component { local.oldestParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"}; } local.oldestSql &= " ORDER BY createdAt ASC"; - local.oldestRow = queryExecute(local.oldestSql, local.oldestParams, {datasource = variables.$datasource, maxrows = 1}); + // Bound in SQL text (see the recentJobs note on driver maxrows) — + // the ORDER BY puts the oldest pending row first. + local.oldestSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = 1); + local.oldestRow = queryExecute(local.oldestSql, local.oldestParams, {datasource = variables.$datasource}); if (local.oldestRow.recordCount) { // This reads through a raw queryExecute, so the Wheels adapter's // date canonicalization never runs. Adobe's JDBC driver returns @@ -339,7 +351,10 @@ component { local.selectParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"}; } local.selectSql &= " ORDER BY failedAt ASC"; - local.failedJobs = queryExecute(local.selectSql, local.selectParams, {datasource = variables.$datasource, maxrows = arguments.limit}); + // Bound in SQL text (BoxLang + PostgreSQL throws on the driver + // maxrows option — setLargeMaxRows is not implemented). + local.selectSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = arguments.limit); + local.failedJobs = queryExecute(local.selectSql, local.selectParams, {datasource = variables.$datasource}); if (!local.failedJobs.recordCount) return 0;