Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,37 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('totalPass','COMPILE FAIL'), d.get('RootCause',{}).get('snippet',''))"
```

17. **A parameter named `default` loses its name — and its declared default value — if a type keyword precedes it, on every Adobe engine.** Adobe treats `default` as reserved in a parameter position, so `string default = ""` registers an argument named **`string`** and discards `default` entirely; the declared default value never materializes in the `arguments` scope. Dropping the type declaration fixes it — `default = ""` (untyped) parses correctly on Adobe, Lucee 6/7 and BoxLang alike.

```cfm
// WRONG — arguments scope gets a key named STRING; `default` never appears
public any function float(string columnNames, string default = "", boolean allowNull = "true") {
// RIGHT — arguments.default exists and carries ""
public any function float(string columnNames, default = "", boolean allowNull = "true") {
```

Explicitly-passed values still arrive (as a separate lowercase `default` key alongside the bogus `STRING` one), which is what makes this so quiet: every call site that passes `default=` works, and only the *declared* default silently vanishes. `TableDefinition.uniqueidentifier()` shipped `string default = "newid()"` and emitted DDL with no `DEFAULT` clause on Adobe for as long as it has existed. All 24 `default` parameter declarations under `vendor/wheels/` were untyped uniformly in the #3302 burn-down; `cli/lucli/services/ArgSpec.cfc` still has typed ones but runs on the Lucee-only LuCLI runtime.

18. **Adobe 2025's `FileWrite()` appends a trailing `0x0A` when handed a simple value.** `FileWrite(path, "hello world")` puts **12** bytes on disk, not 11. Lucee 6/7, BoxLang and Adobe 2023 write the string verbatim, so local Lucee green does not cover this. Harmless for generated source or JSON; fatal anywhere the read must round-trip what was written, which is why it corrupted every object stored through `wheels.storage.drivers.LocalDisk` (#3302). Decode to binary first — the binary overload has no line-ending behaviour on any engine:

```cfm
var payload = IsBinary(content) ? content : CharsetDecode(content, "utf-8");
FileWrite(path, payload);
```

Verify Adobe CF fixes locally before pushing — don't iterate via CI:
```bash
curl -s "http://localhost:62023/wheels/core/tests?db=mysql&format=json" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('totalPass',0),'pass',d.get('totalFail',0),'fail',d.get('totalError',0),'error')"
```

**Adobe serves cached compiled classes — `?reload=true` does NOT pick up an edited `.cfc`.** `?reload=true` rebuilds the Wheels application scope, not Adobe's template cache, so a source change can keep producing the *old* result for many minutes. This reads exactly like a fix that did not work, and the natural response — reverting or piling on a second change — makes it worse. After editing framework source, `docker restart wheels-adobe2023-1` (or `-adobe2025-1`) before trusting any Adobe result. Lucee and BoxLang pick edits up from the bind mount immediately; only the Adobe legs need this.

**Narrow the run with `directory=` — it turns a ~19-minute CI round-trip into ~5 seconds.** The core-test endpoint accepts a dotted TestBox scope, allowlisted to `wheels.tests.*` and `vendor.<package>.tests.*`. `bundles=` is silently ignored (#3352), so `directory=` is the only working filter. Point it at a *directory*, never a single spec file — a single-file scope discovers 0 bundles and reports green (#3083); check `bundlesDiscovered` in the payload.
```bash
curl -s "http://localhost:62025/wheels/core/tests?db=sqlite&directory=wheels.tests.specs.security&format=json&reload=true"
```

Deep reference: [.ai/wheels/cross-engine-compatibility.md](.ai/wheels/cross-engine-compatibility.md).

## Anti-Patterns (Top 14)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/3302-adobe-typed-default-param.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Migrator column helpers no longer lose their declared `default` values on Adobe ColdFusion. A parameter declared as `<type> default` (e.g. `string default = "newid()"`) is parsed by Adobe as a parameter named `string`, silently discarding the `default` name and its declared value — so `t.uniqueidentifier()` emitted DDL with no `DEFAULT` clause and `t.float()` lost its `default=""` / `allowNull=true` outlier defaults. The type keyword has been dropped from every `default` parameter declaration in `Migration.cfc`, `TableDefinition.cfc`, `Abstract.cfc`, the MySQL/SQLite migrators and `DatabaseMigratorAdapterInterface.cfc` (#3302)
1 change: 1 addition & 0 deletions changelog.d/3302-boxlang-evaluate-expression.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `$evaluateExpression()` now evaluates built-in-function expressions through the BoxLang runtime on BoxLang. BoxLang ships no `Evaluate()` BIF, so every expression that fell through to the built-in branch returned `Error evaluating expression: Function [Evaluate] not found` instead of its result (#3302)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `CockroachDBTransactionSpec` now declares an isolation level on the outer transaction that wraps `updateAll(transaction="rollback")`. Adobe ColdFusion rejects a nested `cftransaction` whose isolation level differs from its parent's, and the resulting exception escaped `invokeWithTransaction` before its `catch` could clear `request.wheels.transactions`, leaving the connection permanently marked as "transaction already open" — so every later model call in that request silently skipped its own transaction and `OuterTransactionSignalSpec`'s rollback assertion failed as a knock-on (#3302)
1 change: 1 addition & 0 deletions changelog.d/3302-insert-column-list-parity.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `$parseInsertColumnList()` now uses one implementation on every engine instead of forking on a BoxLang check whose non-BoxLang branch dropped the comma delimiters when it ran on BoxLang. The unified regex form also preserves spaces inside quoted identifiers such as `[order date]`, which the previous `ReplaceList` form stripped (#3302)
1 change: 1 addition & 0 deletions changelog.d/3302-localdisk-binary-write.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `LocalDisk.put()` now writes content as bytes rather than as a string, so `get()` round-trips exactly what was stored. Adobe ColdFusion 2025's `FileWrite()` appends a trailing line feed to simple values, which added a byte to every stored object and corrupted binary payloads (#3302)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Helper functions included into `wheels.Public` by `$init()` are now reachable on the component's `this` scope on every engine. The runtime include placed them in `variables` only, so external callers hit "has no function with name" on Lucee 6, Adobe 2023 and Adobe 2025 while the same call worked on Lucee 7 and BoxLang (#3302)
1 change: 1 addition & 0 deletions changelog.d/3302-transaction-marker-reset.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `invokeWithTransaction()` now clears `request.wheels.transactions` when the `cftransaction` fails to open, not only when the wrapped method throws. A rejected isolation level, a nested-isolation mismatch, or a dead connection previously left the connection marked "transaction already open" for the rest of the request, so every later model call silently ran with no transaction at all (#3302)
18 changes: 18 additions & 0 deletions vendor/wheels/Public.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" {
*/
public struct function $init() {
include "/wheels/public/helpers.cfm";

// The include above declares its UDFs into `variables` only — they never
// reach `this` on Lucee 6, Adobe 2023 or Adobe 2025 (Lucee 7 and BoxLang
// do promote them, which is why the split stayed invisible). Every helper
// in helpers.cfm is declared `public`, and the framework's own views reach
// them through `variables`, so the divergence only bites an external
// caller — `CreateObject("component", "wheels.Public").$init().$$findMatchingRoutes(…)`
// threw "has no function with name" on three of five engines (##3302).
//
// Same problem, same fix as the `/app/global/functions.cfm` include in
// `Global.cfc`'s pseudo-constructor. Call the raw scan rather than
// `$promoteIncludedGlobalsToThis()`: that wrapper memoizes its promote
// list per class in application scope, and the entry for `wheels.Public`
// is written by the pseudo-constructor *before* this include runs — so the
// memoized path would replay a stale, pre-include key list and promote
// nothing. This is the dev-only GUI component, not a request hot path.
$scanAndPromoteIncludedGlobals();

return this;
}

Expand Down
13 changes: 12 additions & 1 deletion vendor/wheels/Test.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,18 @@ component output="false" displayName="Test" extends="wheels.Global"{
if(arrayLen(local.args) == 2){
return invoke(variables, local.functionName[1], variables[local.args[2]]);
} else {
// Use the Evaluate function to run Built-in functions
// Built-in functions. No portable call exists here:
// BoxLang has no Evaluate() BIF at all (verified absent
// on 1.11.0 — "The method Evaluate does not exist"), and
// getBoxRuntime() exists only on BoxLang. executeStatement()
// is the faithful equivalent — like Evaluate it takes the
// whole expression string, so neither branch has to
// re-parse the argument list. Function calls resolve at
// runtime, so the BoxLang-only name never has to compile
// on Lucee or Adobe (#3302).
if (StructKeyExists(server, "boxlang")) {
return getBoxRuntime().executeStatement(arguments.expression);
}
return Evaluate(arguments.expression);
}
}
Expand Down
6 changes: 5 additions & 1 deletion vendor/wheels/channel/DatabaseAdapter.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,12 @@ component {
/**
* Detect the database type from the datasource via JDBC metadata.
* Returns: "oracle", "postgresql", "h2", "mysql", "sqlserver", "sqlite", or "default".
*
* Public with $ prefix (internal naming convention), matching its caller
* $applyRowBound, so a spec can reproduce the dialect the bounded cleanup
* pass actually chose on the engine/database pair it is running against.
*/
private string function $detectDatabaseType() {
public string function $detectDatabaseType() {
try {
cfdbinfo(type="version", datasource="#variables.$datasource#", name="local.info");
local.product = local.info.database_productname;
Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/databaseAdapters/Abstract.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ component extends="wheels.migrator.Base"{
}

// what's the purpose of this?
public boolean function optionsIncludeDefault(string type, string default = "", boolean allowNull = true) {
public boolean function optionsIncludeDefault(string type, default = "", boolean allowNull = true) {
return true;
}

Expand Down
26 changes: 17 additions & 9 deletions vendor/wheels/databaseAdapters/Base.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,23 @@ component output=false extends="wheels.Global"{
local.columnList = "";
if (local.startPar > 1 && local.endPar > local.startPar) {
local.rawColumns = Mid(arguments.sql, local.startPar, (local.endPar - local.startPar));
if ($isBoxLangEngine()) {
// BoxLang's ReplaceList behaves differently — use regex to parse the column names.
local.columnList = REReplace(local.rawColumns, "\s*,\s*", ",", "all");
local.columnList = REReplace(local.columnList, "[\r\n]", "", "all");
local.columnList = Trim(local.columnList);
} else {
// Original Lucee / Adobe CF behavior.
local.columnList = ReplaceList(local.rawColumns, "#Chr(10)#,#Chr(13)#, ", ",,");
}
// One implementation for every engine. This used to fork on
// $isBoxLangEngine(), with the ReplaceList form kept for Lucee/Adobe —
// but BoxLang's ReplaceList drops the comma delimiters themselves, so
// "id,name,age" came back as "idnameage" on any code path that reached
// that branch on BoxLang. BaseProbe hard-codes $isBoxLangEngine() to
// false, so the unit spec drove exactly that branch on the boxlang legs
// and failed on all five databases (#3302) while the sibling spec — which
// sets boxlangMode=true — passed. Collapsing the fork removes both the
// engine-dependent behaviour and the test-double trap.
//
// The regex form is also the more correct of the two: ReplaceList
// stripped every space, mangling quoted identifiers that legitimately
// contain one (e.g. `[order date]`), whereas \s*,\s* only collapses
// whitespace adjacent to the delimiters.
local.columnList = REReplace(local.rawColumns, "\s*,\s*", ",", "all");
local.columnList = REReplace(local.columnList, "[\r\n]", "", "all");
local.columnList = Trim(local.columnList);
}
// Strip identifier quotes from the column list for comparison.
return $stripIdentifierQuotes(local.columnList);
Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/databaseAdapters/MySQL/MySQLMigrator.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ component extends="wheels.databaseAdapters.Abstract" {
* `vendor/wheels/tests/specs/migrator/addColumnOptionsSpec.cfc` — keep
* this list and that spec aligned. See #2742.
*/
public boolean function optionsIncludeDefault(string type, string default = "", boolean allowNull = true) {
public boolean function optionsIncludeDefault(string type, default = "", boolean allowNull = true) {
if (ListFindNoCase("text,mediumtext,longtext,float", arguments.type)) {
return false;
} else {
Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/databaseAdapters/SQLite/SQLiteMigrator.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ component extends="wheels.databaseAdapters.Abstract" {
/**
* In SQLite, most types can have default values, except BLOB.
*/
public boolean function optionsIncludeDefault(string type, string default = "", boolean allowNull = true) {
public boolean function optionsIncludeDefault(string type, default = "", boolean allowNull = true) {
if (ListFindNoCase("blob", arguments.type)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ interface {
* @allowNull Whether NULL is allowed.
* @return True if a DEFAULT clause should be added.
*/
public boolean function optionsIncludeDefault(string type, string default, boolean allowNull);
public boolean function optionsIncludeDefault(string type, default, boolean allowNull);

/**
* Quote a value for use in DDL statements.
Expand Down
4 changes: 2 additions & 2 deletions vendor/wheels/migrator/Migration.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ component extends="Base" {
string columnNames,
string afterColumn = "",
string referenceName = "",
string default,
default,
boolean allowNull,
numeric limit,
numeric precision,
Expand Down Expand Up @@ -213,7 +213,7 @@ component extends="Base" {
required string columnType,
string afterColumn = "",
string referenceName = "",
string default,
default,
boolean allowNull,
numeric limit,
numeric precision,
Expand Down
Loading
Loading