Skip to content
Open

v14 #326

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
413fb58
ci: disable Lucee bleeding-edge checks (#316)
elpete Aug 12, 2026
e4f670d
fix(SchemaBuilder): allow timestamps in alter statements (#314)
elpete Aug 12, 2026
19a5eb8
feat(QueryBuilder): add named return formatters (#315)
elpete Aug 12, 2026
857f124
feat(QueryBuilder): add cross-grammar JSON support (#317)
elpete Aug 12, 2026
2d0d8cc
feat(QueryBuilder): validate duplicate select columns (#318)
elpete Aug 12, 2026
90b2978
feat(QueryBuilder): add whereInBulk (#319)
elpete Aug 12, 2026
dcaede6
fix(SqlServerGrammar): replace defaults when modifying columns (#320)
elpete Aug 12, 2026
d179110
feat(QueryBuilder): add parameter-aware bulk inserts (#321)
elpete Aug 12, 2026
722ec43
fix(SqlServerGrammar): compile ordered union branches (#322)
elpete Aug 12, 2026
54a58d6
fix(SchemaBuilder): qualify tables with default schema (#323)
elpete Aug 12, 2026
1ad11d8
feat(QueryBuilder): match null upsert targets (#324)
elpete Aug 12, 2026
cf5bcd5
feat(moduleconfig): add shouldWrapValues setting (#325)
elpete Aug 12, 2026
edd1786
test(QueryBuilder): cover unlimited grammar parameter limits
elpete Aug 13, 2026
9973ae7
fix(QueryBuilder): address v14 regression audit
elpete Aug 13, 2026
554ca20
fix: resolve query and schema regression audit
elpete Aug 13, 2026
c6df4fa
fix: isolate query state and full-null behavior
elpete Aug 13, 2026
40f8af8
fix: restore query and schema builder lifecycle
elpete Aug 13, 2026
5c686de
fix(QueryBuilder): preserve compiled binding alignment
elpete Aug 13, 2026
fb269a6
fix(QueryBuilder): preserve specialized query behavior
elpete Aug 13, 2026
2b607aa
fix(QueryBuilder): preserve query utility semantics
elpete Aug 13, 2026
619e54f
fix(QueryBuilder): preserve binding ownership
elpete Aug 14, 2026
b640937
fix(Grammar): correct dialect edge cases
elpete Aug 14, 2026
0f375aa
fix: harden query and schema compilation
elpete Aug 14, 2026
7b5ac66
fix: harden query and schema edge cases
elpete Aug 15, 2026
9ccbf9e
fix: preserve nested SQL compilation semantics
elpete Aug 15, 2026
3e747f9
fix: isolate nested query state and DML CTEs
elpete Aug 15, 2026
80396c1
fix: close remaining state ownership gaps
elpete Aug 15, 2026
19d2344
fix: align nested query and binding compilation
elpete Aug 15, 2026
6f0ca88
fix: scope alter view bindings to creation
elpete Aug 15, 2026
edcd3d0
fix: harden dialect parsing edge cases
elpete Aug 15, 2026
9eb8067
fix: preserve Adobe ColdFusion compatibility
elpete Aug 16, 2026
f6e63d8
fix: handle omitted null JSON bindings
elpete Aug 16, 2026
7ac2ca6
ci: exclude unsupported Adobe full-null checks
elpete Aug 16, 2026
fd77bb4
ci: increase stable BoxLang test heap
elpete Aug 16, 2026
d46bcb3
ci: use compact TestBox reports
elpete Aug 16, 2026
504ffa4
fix(QueryBuilder): keep generated methods within JVM limits
elpete Aug 16, 2026
86af77e
fix(QueryBuilder): preserve explicit BoxLang JSON nulls
elpete Aug 16, 2026
1cddb7c
fix(QueryBuilder): normalize full-null JSON shortcuts
elpete Aug 16, 2026
a3ef396
feat(schema): add binary() column type to the schema builder (#327)
lmajano Aug 16, 2026
41efafa
refactor(QueryBuilder): extract lazy collaborators
elpete Aug 16, 2026
da8acd5
fix(QueryBuilder): handle aliases and null edge cases
elpete Aug 16, 2026
cb840d7
fix(JsonQueryClause): preserve BoxLang null containment shortcuts
elpete Aug 16, 2026
9716e1b
fix(SqlServerGrammar): declare aliases in data modifications
elpete Aug 16, 2026
fe0fd86
fix(QueryBuilder): preserve nested query invariants
elpete Aug 16, 2026
97d5414
test(QueryBuilder): split abstract specs by behavior
elpete Aug 16, 2026
866acab
refactor(QueryBuilder): extract join clause manager
elpete Aug 16, 2026
7a02745
refactor(QueryBuilder): extract predicate clause
elpete Aug 16, 2026
f12710a
fix: resolve query and schema regressions
elpete Aug 17, 2026
1958d81
fix: support Adobe null predicate arguments
elpete Aug 17, 2026
8e10683
fix: support concurrent Adobe compilation
elpete Aug 17, 2026
226cfce
fix: harden query edge case handling
elpete Aug 17, 2026
d98fbed
fix: keep failed clause mutations atomic
elpete Aug 17, 2026
f2f3ae8
fix: preserve query state after binding errors
elpete Aug 17, 2026
2c8bb45
fix(QueryUtils): preserve BigDecimal scale on BoxLang
elpete Aug 17, 2026
0efdc0f
fix(QueryUtils): handle nested PostgreSQL comments
elpete Aug 17, 2026
9e7ec5b
fix(QueryBuilder): replace bindings between updates
elpete Aug 17, 2026
1a776b4
fix(QueryBuilder): replace bindings between inserts
elpete Aug 17, 2026
b24f23f
fix(QueryBuilder): replace bindings between insert ignores
elpete Aug 17, 2026
e2bc3f7
fix(QueryBuilder): preserve orders after failed reorder
elpete Aug 17, 2026
b0b5a60
fix(QueryBuilder): preserve returning state on errors
elpete Aug 17, 2026
19df3a2
fix(QueryBuilder): restore bindings after failed upserts
elpete Aug 17, 2026
16798c1
fix(QueryBuilder): restore bindings after failed insert using
elpete Aug 17, 2026
6539010
fix(QueryBuilder): restore bindings after failed bulk inserts
elpete Aug 17, 2026
361cc38
fix(QueryUtils): infer byte values as integers
elpete Aug 17, 2026
34305ed
fix(QueryUtils): infer Java dates as timestamps
elpete Aug 17, 2026
a79c662
fix(SQLCommenter): handle Oracle quoted literals
elpete Aug 17, 2026
af32769
fix(JsonQueryClause): parse negative array indexes
elpete Aug 17, 2026
e136c45
fix(QueryBuilder): keep raw order bindings atomic
elpete Aug 17, 2026
caf4ff0
fix(QueryUtils): normalize prefixed SQL types
elpete Aug 17, 2026
c7af550
fix(PostgresGrammar): preserve text default colons
elpete Aug 17, 2026
533ee64
fix(QueryUtils): render Boolean SQL literals
elpete Aug 17, 2026
28eaa79
fix(QueryBuilder): clamp pagination page offsets
elpete Aug 17, 2026
fc32148
fix(QueryBuilder): clamp negative row bounds
elpete Aug 17, 2026
7653fb7
fix(PredicateClause): preserve null IN values
elpete Aug 17, 2026
015f4a2
fix(QueryBuilder): preserve null expression bindings
elpete Aug 17, 2026
86042bc
fix(QueryUtils): render CLOB values as text
elpete Aug 17, 2026
f740508
fix(QueryBuilder): restore value query columns
elpete Aug 17, 2026
686159a
fix(QueryUtils): normalize inferred SQL types
elpete Aug 17, 2026
b44c493
fix(QueryBuilder): normalize pagination metadata pages
elpete Aug 17, 2026
2c5c767
fix(QueryBuilder): preserve pretend mode on reset
elpete Aug 17, 2026
9589833
fix(QueryBuilder): omit wildcard insert targets
elpete Aug 17, 2026
71b9e42
fix(OracleGrammar): preserve user row number columns
elpete Aug 17, 2026
59ee441
fix(DerbyGrammar): preserve user row number columns
elpete Aug 17, 2026
89b3c16
fix(QueryBuilder): prevent duplicate cross joins
elpete Aug 17, 2026
a7e070a
fix(SchemaBuilder): apply configured default options
elpete Aug 17, 2026
e5bcf53
fix(PostgresGrammar): compile multi-join updates
elpete Aug 17, 2026
993f6bb
fix(SQLiteGrammar): compile multi-join updates
elpete Aug 17, 2026
f1d6044
fix(SQLiteGrammar): order update row limits
elpete Aug 17, 2026
d453430
fix(MySQLGrammar): preserve delete row selection
elpete Aug 17, 2026
90082dd
fix(SQLiteGrammar): preserve delete row selection
elpete Aug 17, 2026
39f61ec
fix(MySQLGrammar): preserve update row selection
elpete Aug 17, 2026
c703b20
fix(SQLiteGrammar): preserve update ordering
elpete Aug 17, 2026
9cfe117
fix(MySQLGrammar): reject DML offsets
elpete Aug 17, 2026
6403901
fix(MySQLGrammar): reject joined delete row selection
elpete Aug 17, 2026
882b9be
fix(SqlServerGrammar): preserve delete limits
elpete Aug 17, 2026
184bc63
fix(SqlServerGrammar): reject unsupported DML selection
elpete Aug 17, 2026
654a4b3
fix(PostgresGrammar): reject unsupported DML selection
elpete Aug 17, 2026
ffe83e6
fix(OracleGrammar): reject unsupported DML selection
elpete Aug 17, 2026
e7cb622
fix(DerbyGrammar): reject unsupported DML selection
elpete Aug 17, 2026
7e2118e
fix(QueryUtils): normalize untyped null bindings
elpete Aug 17, 2026
55db41d
fix(SQLiteGrammar): reject added auto-increment columns
elpete Aug 17, 2026
1a21613
fix(SQLiteGrammar): reject added unique columns
elpete Aug 17, 2026
e05a67b
fix(SQLiteGrammar): reject added stored columns
elpete Aug 17, 2026
6b17a36
ci: enable Adobe full-null test matrix
elpete Aug 17, 2026
b5740d5
test: support Adobe row number regressions
elpete Aug 17, 2026
f912f2a
fix(SQLiteGrammar): support Adobe array compilation
elpete Aug 17, 2026
d4edeae
Apply cfformat changes
elpete Aug 17, 2026
af2305b
ci: check formatting without auto-commit
elpete Aug 18, 2026
2cb91a8
ci: patch TestBox for Adobe full null
elpete Aug 18, 2026
3f5f837
ci: complete TestBox full null patch
elpete Aug 18, 2026
ee40cff
fix: support Adobe full null execution
elpete Aug 18, 2026
756b3c1
fix: guard missing TestBox skip metadata
elpete Aug 18, 2026
229464d
ci: run experimental engines with full null support
elpete Aug 18, 2026
4d50ae0
fix(QueryBuilder): support external subclass collaborators
elpete Aug 22, 2026
28879b1
fix(QueryUtils): infer BIGINT for large integers
elpete Aug 22, 2026
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
69 changes: 69 additions & 0 deletions .github/patches/testbox-full-null.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
diff --git a/testbox/system/coverage/CoverageService.cfc b/testbox/system/coverage/CoverageService.cfc
--- a/testbox/system/coverage/CoverageService.cfc
+++ b/testbox/system/coverage/CoverageService.cfc
@@ -175 +175 @@
- if ( isNull( opts.coverageTresholds ) ) {
+ if ( !structKeyExists( opts, "coverageTresholds" ) || isNull( opts.coverageTresholds ) ) {
@@ -178 +178 @@
- if ( isNull( opts.coverageTresholds.good ) ) {
+ if ( !structKeyExists( opts.coverageTresholds, "good" ) || isNull( opts.coverageTresholds.good ) ) {
@@ -181 +181 @@
- if ( isNull( opts.coverageTresholds.bad ) ) {
+ if ( !structKeyExists( opts.coverageTresholds, "bad" ) || isNull( opts.coverageTresholds.bad ) ) {
diff --git a/testbox/system/TestBox.cfc b/testbox/system/TestBox.cfc
--- a/testbox/system/TestBox.cfc
+++ b/testbox/system/TestBox.cfc
@@ -408 +408 @@
- if ( !isNull( url.testBundles ) ) {
+ if ( structKeyExists( url, "testBundles" ) && !isNull( url.testBundles ) ) {
@@ -411 +411 @@
- if ( !isNull( url.testSuites ) ) {
+ if ( structKeyExists( url, "testSuites" ) && !isNull( url.testSuites ) ) {
@@ -414 +414 @@
- if ( !isNull( url.testSpecs ) ) {
+ if ( structKeyExists( url, "testSpecs" ) && !isNull( url.testSpecs ) ) {
@@ -417 +417 @@
- if ( !isNull( url.testMethod ) ) {
+ if ( structKeyExists( url, "testMethod" ) && !isNull( url.testMethod ) ) {
@@ -259 +259 @@
- if ( isNull( variables.env ) ) {
+ if ( !structKeyExists( variables, "env" ) || isNull( variables.env ) ) {
diff --git a/testbox/system/util/Util.cfc b/testbox/system/util/Util.cfc
--- a/testbox/system/util/Util.cfc
+++ b/testbox/system/util/Util.cfc
@@ -203 +203 @@
- if ( isNull( variables.engineMappingHelper ) ) {
+ if ( !structKeyExists( variables, "engineMappingHelper" ) || isNull( variables.engineMappingHelper ) ) {
diff --git a/testbox/system/util/Env.cfc b/testbox/system/util/Env.cfc
--- a/testbox/system/util/Env.cfc
+++ b/testbox/system/util/Env.cfc
@@ -87 +87 @@
- if ( isNull( variables.javaSystem ) ) {
+ if ( !structKeyExists( variables, "javaSystem" ) || isNull( variables.javaSystem ) ) {
diff --git a/testbox/system/BaseSpec.cfc b/testbox/system/BaseSpec.cfc
--- a/testbox/system/BaseSpec.cfc
+++ b/testbox/system/BaseSpec.cfc
@@ -1627 +1627 @@
- if ( isNull( variables.$cbMockData ) ) {
+ if ( !structKeyExists( variables, "$cbMockData" ) || isNull( variables.$cbMockData ) ) {
@@ -1640 +1640 @@
- if ( isNull( variables.$utility ) ) {
+ if ( !structKeyExists( variables, "$utility" ) || isNull( variables.$utility ) ) {
@@ -1653 +1653 @@
- if ( isNull( variables.$env ) ) {
+ if ( !structKeyExists( variables, "$env" ) || isNull( variables.$env ) ) {
@@ -1668 +1668 @@
- if ( isNull( this.$mockbox ) ) {
+ if ( !structKeyExists( this, "$mockbox" ) || isNull( this.$mockbox ) ) {
diff --git a/testbox/system/runners/BDDRunner.cfc b/testbox/system/runners/BDDRunner.cfc
--- a/testbox/system/runners/BDDRunner.cfc
+++ b/testbox/system/runners/BDDRunner.cfc
@@ -159 +159 @@
- isNull( thisSuite ) ? {} : thisSuite
+ !structKeyExists( local, "thisSuite" ) || isNull( local.thisSuite ) ? {} : local.thisSuite
diff --git a/testbox/system/runners/BaseRunner.cfc b/testbox/system/runners/BaseRunner.cfc
--- a/testbox/system/runners/BaseRunner.cfc
+++ b/testbox/system/runners/BaseRunner.cfc
@@ -275 +275 @@
- if ( isNull( md.skip ) ) {
+ if ( !md.keyExists( "skip" ) || isNull( md.skip ) ) {
7 changes: 4 additions & 3 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ jobs:
experimental: [ false ]
fullNull: ["true", "false"]
include:
- cfengine: "lucee@be"
experimental: true
- cfengine: "adobe@be"
experimental: true
- cfengine: "boxlang@be"
Expand All @@ -40,8 +38,11 @@ jobs:
- name: Install dependencies
run: |
box install
git apply --unidiff-zero .github/patches/testbox-full-null.patch

- name: Start server
env:
FULL_NULL: ${{matrix.fullNull}}
run: |
box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug
curl http://127.0.0.1:60298
Expand All @@ -50,4 +51,4 @@ jobs:
env:
FULL_NULL: ${{matrix.fullNull}}
continue-on-error: ${{ matrix.experimental }}
run: box testbox run
run: box testbox run reporter=mintext
16 changes: 7 additions & 9 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ jobs:
experimental: [ false ]
fullNull: ["true", "false"]
include:
- cfengine: "lucee@be"
experimental: true
- cfengine: "adobe@be"
experimental: true
fullNull: "true"
- cfengine: "boxlang@be"
experimental: true
fullNull: "true"
steps:
- name: Checkout Repository
uses: actions/checkout@v7
Expand All @@ -48,8 +48,11 @@ jobs:
- name: Install dependencies
run: |
box install
git apply --unidiff-zero .github/patches/testbox-full-null.patch

- name: Start server
env:
FULL_NULL: ${{matrix.fullNull}}
run: |
box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug
curl http://127.0.0.1:60298
Expand All @@ -58,7 +61,7 @@ jobs:
env:
FULL_NULL: ${{matrix.fullNull}}
continue-on-error: ${{ matrix.experimental }}
run: box testbox run
run: box testbox run reporter=mintext

format:
runs-on: ubuntu-latest
Expand All @@ -80,9 +83,4 @@ jobs:
run: box install commandbox-cfformat

- name: Run CFFormat
run: box run-script format

- name: Commit Format Changes
uses: stefanzweifel/git-auto-commit-action@v7.2.0
with:
commit_message: Apply cfformat changes
run: box run-script format:check
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ jobs:
- name: Install dependencies
run: |
box install
git apply --unidiff-zero .github/patches/testbox-full-null.patch

- name: Start server
env:
FULL_NULL: ${{matrix.fullNull}}
run: |
box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug
curl http://127.0.0.1:60298
Expand All @@ -46,7 +49,7 @@ jobs:
env:
FULL_NULL: ${{matrix.fullNull}}
continue-on-error: ${{ matrix.experimental }}
run: box testbox run
run: box testbox run reporter=mintext

release:
name: Semantic Release
Expand Down Expand Up @@ -104,4 +107,4 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_SECRET }}
SOURCE_DIR: ".tmp/apidocs"
DEST_DIR: "${{ github.repository }}/${{ steps.current_version.outputs.version }}"
DEST_DIR: "${{ github.repository }}/${{ steps.current_version.outputs.version }}"
29 changes: 25 additions & 4 deletions ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ component {
"defaultReturnFormat": "array",
"preventDuplicateJoins": false,
"validateOperatorsAndCombinators": true,
"validateDuplicateSelectColumns": false,
"validateQueryExecuteReturnType": false,
"collectQueryLog": true,
"convertEmptyStringsToNull": true,
"shouldWrapValues": true,
"validateQueryParamStructKeys": true,
"numericSQLType": "NUMERIC",
"integerSQLType": "INTEGER",
"bigIntegerSQLType": "BIGINT",
"decimalSQLType": "DECIMAL",
"defaultOptions": {},
"sqlCommenter": {
Expand All @@ -29,7 +32,8 @@ component {
},
"shouldMaxRowsOverrideToAll": function( maxRows ) {
return maxRows <= 0;
}
},
"returnFormatters": {}
};

interceptorSettings = { "customInterceptionPoints": "preQBExecute,postQBExecute" };
Expand All @@ -51,17 +55,26 @@ component {
.to( "qb.models.Query.QueryUtils" )
.initArg( name = "convertEmptyStringsToNull", value = settings.convertEmptyStringsToNull )
.initArg( name = "validateQueryParamStructKeys", value = settings.validateQueryParamStructKeys )
.initArg( name = "numericSQLType", value = settings.numericSQLType )
.initArg( name = "integerSQLType", value = settings.integerSQLType )
.initArg( name = "bigIntegerSQLType", value = settings.bigIntegerSQLType )
.initArg( name = "decimalSQLType", value = settings.decimalSQLType );

binder
.map( alias = "ReturnFormatterRegistry@qb", force = true )
.to( "qb.models.Query.ReturnFormatterRegistry" )
.initArg( name = "utils", ref = "QueryUtils@qb" )
.initArg( name = "returnFormatters", value = settings.returnFormatters );

binder
.map( alias = "QueryBuilder@qb", force = true )
.to( "qb.models.Query.QueryBuilder" )
.initArg( name = "grammar", ref = settings.defaultGrammar )
.initArg( name = "utils", ref = "QueryUtils@qb" )
.initArg( name = "returnFormatterRegistry", ref = "ReturnFormatterRegistry@qb" )
.initArg( name = "preventDuplicateJoins", value = settings.preventDuplicateJoins )
.initArg( name = "validateOperatorsAndCombinators", value = settings.validateOperatorsAndCombinators )
.initArg( name = "validateDuplicateSelectColumns", value = settings.validateDuplicateSelectColumns )
.initArg( name = "validateQueryExecuteReturnType", value = settings.validateQueryExecuteReturnType )
.initArg( name = "collectQueryLog", value = settings.collectQueryLog )
.initArg( name = "returnFormat", value = settings.defaultReturnFormat )
.initArg( name = "defaultOptions", value = settings.defaultOptions )
Expand All @@ -71,7 +84,15 @@ component {
binder
.map( alias = "SchemaBuilder@qb", force = true )
.to( "qb.models.Schema.SchemaBuilder" )
.initArg( name = "grammar", ref = settings.defaultGrammar );
.initArg( name = "grammar", ref = settings.defaultGrammar )
.initArg( name = "defaultOptions", value = settings.defaultOptions );

// Apply shouldWrapValues setting to the configured grammar singleton.
// When defaultGrammar is AutoDiscover@qb, the setting is forwarded via
// onMissingMethod to whatever concrete grammar AutoDiscover resolves at runtime.
if ( structKeyExists( settings, "shouldWrapValues" ) ) {
wirebox.getInstance( settings.defaultGrammar ).setShouldWrapValues( settings.shouldWrapValues );
}
}

}
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,88 @@ q = queryExecute(

qb enables you to explore new ways of organizing your code by letting you pass around a query builder object that will compile down to the right SQL without you having to keep track of the order, whitespace, or other SQL gotchas!

## Bulk WHERE IN

For large value collections, `whereInBulk` serializes the values into one bound parameter and lets the active grammar expand them into rows. This avoids database parameter limits without changing the behavior or performance of regular `whereIn` calls.

```cfc
query
.from( "users" )
.whereInBulk( "id", userIds )
.get();
```

qb infers a common type from the values and translates it to the active database grammar. Matching `cfsqltype` values in query parameter structs are preserved. Mixed values fall back to the grammar's string type.

You can pass an explicit `sqlType` as the third argument when the column needs a more specific database type, such as `BIGINT`, `UUID`, or a particular decimal precision:

```cfc
query
.from( "users" )
.whereInBulk( "id", userIds, "BIGINT" )
.get();
```

The explicit `sqlType` should match the constrained column so the database can avoid implicit conversions. `whereNotInBulk`, `andWhereInBulk`, `orWhereInBulk`, `andWhereNotInBulk`, and `orWhereNotInBulk` are also available.

Bulk value expansion is supported by these grammars and database features:

+ SQL Server 2016+ using `OPENJSON`; database compatibility level 130+ is required
+ PostgreSQL 9.4+ using `JSONB_ARRAY_ELEMENTS_TEXT`
+ MySQL 8.0.4+ and MariaDB 10.6+ using `JSON_TABLE`
+ Oracle Database 12c Release 1 (12.1.0.2)+ using `JSON_TABLE`
+ SQLite with JSON functions enabled; they are built in by default as of SQLite 3.38.0

Derby does not support bulk value expansion and throws an `UnsupportedOperation` exception for non-empty collections.

## Development Validation

qb can detect statically identifiable duplicate select output names before they are silently collapsed by CFML query results. Enable this validation in development and leave it disabled in production:

```cfc
moduleSettings = {
"qb": {
"validateDuplicateSelectColumns": true
}
};
```

The validation checks the final selection when the query is compiled, including simple columns, explicit aliases, subselect aliases, and explicitly aliased typed columns. Wildcards and expressions without explicit aliases are skipped because their output names cannot be known until the query executes.

## Return Formatters

qb includes named return formatters for `array`, `query`, `none`, and `struct`. The `struct` formatter returns a struct of rows keyed by a selected column:

```cfc
usersByUsername = query
.setReturnFormat( "struct", { "columnKey": "username" } )
.from( "users" )
.get();
```

Applications can register reusable custom formatter factories in their qb module settings:

```cfc
moduleSettings = {
"qb": {
"returnFormatters": {
"ids": function( options ) {
return function( q ) {
return queryColumnData( q, options.column );
};
}
}
}
};

ids = query
.setReturnFormat( "ids", { "column": "id" } )
.from( "users" )
.get();
```

Formatter factories can also be WireBox mapping names or components with a `toFormatter( options )` method.

Here's a gist with an example of the powerful models you can create with this!
https://gist.github.com/elpete/80d641b98025f16059f6476561d88202

Expand Down Expand Up @@ -118,4 +200,3 @@ For both Lucee and ACF you need to set the JDBC Driver class to `org.sqlite.JDBC
## Full Docs

You can browse the full documentation at https://qb.ortusbooks.com

Loading