diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 608235a3..e12b2e67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,8 @@ jobs: done - name: Check committed SQLite web assets run: dart run tool/build_sqlite_web.dart --check + - name: Generate and validate API documentation + run: dart doc --validate-links - name: Test SQLite, PostgreSQL, MySQL and MariaDB run: dart test --concurrency=1 diff --git a/AGENTS.md b/AGENTS.md index bc475a7a..60fef8ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,12 @@ # Dart ORM -This branch is a new implementation. The design reference is -`research/new-dart-orm-design.md`; old code and upstream architecture are retired. +The current implementation is documented in `doc/README.md` and public Dartdoc. +Old code and upstream architecture are retired. - Keep one product package. Add abstractions only to support real use cases. +- Use independent Dart libraries; never use `part` or `part of`. +- Keep implementation in `lib/src/` and public entrypoints as explicit exports. +- Document public behavior, ownership and failure boundaries with Dartdoc. - Use Dart 3.13 stable syntax, static generation, explicit sessions and typed selections. - Table identity is independent of Dart record identity. - Parameterize values, quote identifiers, and validate SQL scope before execution. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae97d24..0fcc7f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 6.0.0-beta.2 + +- Replace shared `part` libraries with independent modules and explicit public + exports. Internal compiler and execution details stay outside the documented API. +- Make `first()` require a row; use `firstOrNull()` for the previous optional + behavior. Add `singleOrNull()` to queries and returning mutations. +- Keep physical schema metadata in `schema_model.dart`; `schema.dart` exposes + model declarations, value types and declaration options. +- Give MySQL and MariaDB their own driver/configuration exports; import the + corresponding engine entrypoint instead of obtaining both from `mysql.dart`. +- Rebuild the guides and public Dartdoc, including API categories, resource + ownership and execution boundaries. Remove archived exploration artifacts. + ## 6.0.0-beta.1 First beta of the new Dart-native ORM. Requires Dart 3.13 or newer. @@ -21,7 +34,7 @@ model/API migration; updating the dependency alone is not sufficient. - Add typed selections, explicit relation loading, transactions, query subscriptions, SQL inspection and capability-checked execution controls. -See [database and platform boundaries](doc/capabilities.md) before adopting the +See [database and platform boundaries](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md) before adopting the beta. MySQL/MariaDB DDL is non-atomic. Cancellation and streaming depend on the selected driver; the default Linux SQLite asset does not expose interruption. diff --git a/README.md b/README.md index d8d61f97..ee9a9a5f 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,18 @@ Declare immutable Dart models, query exactly the fields you need, and keep your schema and migrations in Dart. SQLite, PostgreSQL, MySQL and MariaDB share a typed query API, with explicit database capabilities and transaction boundaries. -[Get started](#get-started) · [Documentation](doc/README.md) · -[Examples](example) · [pub.dev](https://pub.dev/packages/orm/versions/6.0.0-beta.1) +[Get started](#get-started) · [Guides](https://github.com/medz/dart-orm/blob/main/doc/README.md) · +[API reference](https://pub.dev/documentation/orm/6.0.0-beta.2/) · +[Examples](https://github.com/medz/dart-orm/tree/main/example) · [pub.dev](https://pub.dev/packages/orm/versions/6.0.0-beta.2) > **6.0 beta:** a new implementation requiring Dart 3.13+. This is a breaking -> replacement for the Prisma-based 5.x client. Read the [release notes](CHANGELOG.md) +> replacement for the Prisma-based 5.x client. Read the [release notes](https://github.com/medz/dart-orm/blob/main/CHANGELOG.md) > before upgrading an existing application. +Upgrading from beta.1: `first()` now requires a row. Use `firstOrNull()` when an +empty result is expected. See the [beta.2 changes](https://github.com/medz/dart-orm/blob/main/CHANGELOG.md#600-beta2) +for export changes and the new `singleOrNull()` API. + ## Get started Create a Dart application and initialize SQLite: @@ -20,7 +25,7 @@ Create a Dart application and initialize SQLite: ```sh dart create -t console my_app cd my_app -dart pub add orm:^6.0.0-beta.1 +dart pub add orm:^6.0.0-beta.2 dart run orm init --database sqlite ``` @@ -84,10 +89,14 @@ Full-row queries return your model class. A scalar selection returns its value; own DTO. Create and patch inputs distinguish omission, a value, SQL NULL and a database default. +Use `get()` for a list, `first()` for a required first row, and `single()` when +exactly one row must exist. `firstOrNull()` and `singleOrNull()` explicitly allow +an empty result. Selecting a nullable column keeps its nullable Dart type. + Relationships use declared keys. Select nested results explicitly: to-one relationships can join, and collections use parameter-aware batches. There are -no lazy property reads that quietly issue SQL. See [relationships](doc/relations.md) -and the [query cookbook](example/queries.dart). +no lazy property reads that quietly issue SQL. See [relationships](https://github.com/medz/dart-orm/blob/main/doc/relations.md) +and the [query cookbook](https://github.com/medz/dart-orm/blob/main/example/queries.dart). Transactions use the provided `tx` session. Query subscriptions emit snapshots after relevant committed writes. Inspect SQL without connecting, or use raw and @@ -108,11 +117,11 @@ default. `init --database` accepts `sqlite`, `postgres`, `mysql` and `mariadb`. Each migration history belongs to one engine and stores only that engine's reviewed steps and frozen schema. MySQL/MariaDB DDL uses recovery checkpoints -because it can commit implicitly. See [migrations](doc/migrations.md). +because it can commit implicitly. See [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md). Capabilities are explicit. MySQL/MariaDB do not support cursor streaming or token cancellation; their statement timeout discards the connection. Default Linux -SQLite lacks interruption. See [capabilities](doc/capabilities.md) for exact numeric +SQLite lacks interruption. See [capabilities](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md) for exact numeric limits, database versions and platforms that have not been verified. ## Dart and Flutter, native and web @@ -123,8 +132,8 @@ apps provide their own filesystem path, while browsers use named OPFS storage. Flutter Web bundles the SQLite worker and WASM assets automatically. Plain Dart Web exports the same resources with `dart run orm web-assets`. No separate -`orm_flutter` package is needed. Start with the [Flutter example](example/flutter) -or the [SQLite Web guide](doc/sqlite-web.md). +`orm_flutter` package is needed. Start with the [Flutter example](https://github.com/medz/dart-orm/tree/main/example/flutter) +or the [SQLite Web guide](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md). ## One package, independent libraries @@ -139,15 +148,15 @@ Use the layer your application needs: | `schema.dart`, `generate.dart`, `migrate.dart`, `cli.dart` | Declarations, generation, migration and project tooling | Compile typed SQL offline, use a driver without model generation, or run saved -migrations without importing today's application models. See [API boundaries](doc/api.md). +migrations without importing today's application models. See [API boundaries](https://github.com/medz/dart-orm/blob/main/doc/api.md). ## Go further -- [Model declarations and codecs](doc/authoring.md) · [Types](doc/types.md) -- [Queries and pagination](doc/queries.md) · [Relations](doc/relations.md) -- [Transactions and execution](doc/execution.md) · [Subscriptions](doc/watch.md) -- [CLI](doc/cli.md) · [build_runner](doc/generation.md) · [Existing databases](doc/importing.md) -- [SQL inspection](doc/observability.md) · [Named SQL](doc/named-sql.md) -- [Contributing and validation](doc/contributing.md) +- [Model declarations and codecs](https://github.com/medz/dart-orm/blob/main/doc/authoring.md) · [Types](https://github.com/medz/dart-orm/blob/main/doc/types.md) +- [Queries and pagination](https://github.com/medz/dart-orm/blob/main/doc/queries.md) · [Relations](https://github.com/medz/dart-orm/blob/main/doc/relations.md) +- [Transactions and execution](https://github.com/medz/dart-orm/blob/main/doc/execution.md) · [Subscriptions](https://github.com/medz/dart-orm/blob/main/doc/watch.md) +- [CLI](https://github.com/medz/dart-orm/blob/main/doc/cli.md) · [build_runner](https://github.com/medz/dart-orm/blob/main/doc/generation.md) · [Existing databases](https://github.com/medz/dart-orm/blob/main/doc/importing.md) +- [SQL inspection](https://github.com/medz/dart-orm/blob/main/doc/observability.md) · [Named SQL](https://github.com/medz/dart-orm/blob/main/doc/named-sql.md) +- [Contributing and validation](https://github.com/medz/dart-orm/blob/main/doc/contributing.md) -Licensed under the [BSD 3-Clause License](LICENSE). +Licensed under the [BSD 3-Clause License](https://github.com/medz/dart-orm/blob/main/LICENSE). diff --git a/analysis_options.yaml b/analysis_options.yaml index dc50580e..a614954c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,5 @@ include: package:lints/recommended.yaml analyzer: - exclude: - - research/** language: strict-casts: true strict-inference: true diff --git a/assets/sqlite/worker-50917250e3562557e14927df4611d5d5a01a3dbc9bb641a0fa517a3d784e3819.js b/assets/sqlite/worker-0c84ece1c73ab998fbc2c37fe03c3ea9138bf2a717039553dcabf80481929eb1.js similarity index 94% rename from assets/sqlite/worker-50917250e3562557e14927df4611d5d5a01a3dbc9bb641a0fa517a3d784e3819.js rename to assets/sqlite/worker-0c84ece1c73ab998fbc2c37fe03c3ea9138bf2a717039553dcabf80481929eb1.js index ffd5cec7..ca281e29 100644 --- a/assets/sqlite/worker-50917250e3562557e14927df4611d5d5a01a3dbc9bb641a0fa517a3d784e3819.js +++ b/assets/sqlite/worker-0c84ece1c73ab998fbc2c37fe03c3ea9138bf2a717039553dcabf80481929eb1.js @@ -22,7 +22,7 @@ a[c]=function(){if(a[b]===s){a[b]=d()}a[c]=function(){return this[b]} return a[b]}}function lazyFinal(a,b,c,d){var s=a a[b]=s a[c]=function(){if(a[b]===s){var r=d() -if(a[b]!==s){A.n0(b)}a[b]=r}var q=a[b] +if(a[b]!==s){A.n3(b)}a[b]=r}var q=a[b] a[c]=function(){return q} return q}}function makeConstList(a,b){if(b!=null)A.x(a,b) a.$flags=7 @@ -30,10 +30,10 @@ return a}function convertToFastObject(a){function t(){}t.prototype=a new t() return a}function convertAllToFastObject(a){for(var s=0;s4294967295)throw A.d(A.P(a,0,4294967295,"length",null)) @@ -89,7 +89,7 @@ if(typeof a!="object"){if(typeof a=="function")return J.aB.prototype if(typeof a=="symbol")return J.bw.prototype if(typeof a=="bigint")return J.U.prototype return a}if(a instanceof A.e)return a -return J.hd(a)}, +return J.h1(a)}, cL(a){if(typeof a=="string")return J.aS.prototype if(a==null)return a if(Array.isArray(a))return J.r.prototype @@ -97,20 +97,20 @@ if(typeof a!="object"){if(typeof a=="function")return J.aB.prototype if(typeof a=="symbol")return J.bw.prototype if(typeof a=="bigint")return J.U.prototype return a}if(a instanceof A.e)return a -return J.hd(a)}, +return J.h1(a)}, e2(a){if(a==null)return a if(Array.isArray(a))return J.r.prototype if(typeof a!="object"){if(typeof a=="function")return J.aB.prototype if(typeof a=="symbol")return J.bw.prototype if(typeof a=="bigint")return J.U.prototype return a}if(a instanceof A.e)return a -return J.hd(a)}, -mJ(a){if(typeof a=="number")return J.bv.prototype +return J.h1(a)}, +mH(a){if(typeof a=="number")return J.bv.prototype if(typeof a=="string")return J.aS.prototype if(a==null)return a if(!(a instanceof A.e))return J.bg.prototype return a}, -mK(a){if(typeof a=="string")return J.aS.prototype +mI(a){if(typeof a=="string")return J.aS.prototype if(a==null)return a if(!(a instanceof A.e))return J.bg.prototype return a}, @@ -119,14 +119,14 @@ if(typeof a!="object"){if(typeof a=="function")return J.aB.prototype if(typeof a=="symbol")return J.bw.prototype if(typeof a=="bigint")return J.U.prototype return a}if(a instanceof A.e)return a -return J.hd(a)}, +return J.h1(a)}, ab(a,b){if(a==null)return b==null if(typeof a!="object")return b!=null&&a===b return J.bo(a).I(a,b)}, kk(a,b){return J.e2(a).t(a,b)}, kl(a){return J.jG(a).bt(a)}, ii(a,b,c){return J.jG(a).bu(a,b,c)}, -km(a,b){return J.mJ(a).m(a,b)}, +km(a,b){return J.mH(a).m(a,b)}, ij(a,b){return J.e2(a).H(a,b)}, ag(a){return J.bo(a).gu(a)}, cN(a){return J.e2(a).gG(a)}, @@ -134,7 +134,7 @@ ac(a){return J.cL(a).gk(a)}, kn(a){return J.bo(a).gF(a)}, ko(a,b,c){return J.e2(a).ak(a,b,c)}, kp(a,b){return J.e2(a).aq(a,b)}, -hv(a,b,c){return J.mK(a).a1(a,b,c)}, +hv(a,b,c){return J.mI(a).a1(a,b,c)}, b4(a){return J.bo(a).i(a)}, d3:function d3(){}, d5:function d5(){}, @@ -167,7 +167,7 @@ return a^a>>>6}, hL(a){a=a+((a&67108863)<<3)&536870911 a^=a>>>11 return a+((a&16383)<<15)&536870911}, -h9(a,b,c){return a}, +fY(a,b,c){return a}, i9(a){var s,r for(s=$.a9.length,r=0;r=s)return A.ew(b,s,a,q,r) return new A.aC(q,q,!0,b,r,"Value not in range")}, -mG(a,b,c){if(a>c)return A.P(a,0,c,"start",null) +mE(a,b,c){if(a>c)return A.P(a,0,c,"start",null) if(b!=null)if(bc)return A.P(b,a,c,"end",null) return new A.ah(!0,b,"end",null)}, -i4(a){return new A.ah(!0,a,null,null)}, +i3(a){return new A.ah(!0,a,null,null)}, d(a){return A.C(a,new Error())}, C(a,b){var s if(a==null)a=new A.aE() b.dartException=a -s=A.n1 +s=A.n4 if("defineProperty" in Object){Object.defineProperty(b,"message",{get:s}) b.name=""}else b.toString=s return b}, -n1(){return J.b4(this.dartException)}, +n4(){return J.b4(this.dartException)}, u(a,b){throw A.C(a,b==null?new Error():b)}, l(a,b,c){var s if(b==null)b=0 @@ -370,10 +370,10 @@ if(a==null)return new A.eF(a) if(a instanceof A.bY){s=a.a return A.b3(a,s==null?A.bl(s):s)}if(typeof a!=="object")return a if("dartException" in a)return A.b3(a,a.dartException) -return A.my(a)}, +return A.mw(a)}, b3(a,b){if(t.Q.b(b))if(b.$thrownJsError==null)b.$thrownJsError=a return b}, -my(a){var s,r,q,p,o,n,m,l,k,j,i,h,g +mw(a){var s,r,q,p,o,n,m,l,k,j,i,h,g if(!("message" in a))return a s=a.message if("number" in a&&typeof a.number=="number"){r=a.number @@ -420,10 +420,10 @@ bN(a,b){var s if(a==null)return null s=a.$identity if(!!s)return s -s=A.mE(a,b) +s=A.mC(a,b) a.$identity=s return s}, -mE(a,b){var s +mC(a,b){var s switch(b){case 0:s=a.$0 break case 1:s=a.$1 @@ -490,7 +490,7 @@ if($.ip==null)$.ip=A.im("receiver") s=b.length r=A.ku(s,c,a,b) return r}, -i5(a){return A.kw(a)}, +i4(a){return A.kw(a)}, kq(a,b){return A.cE(v.typeUniverse,A.aw(a.a),b)}, iq(a){return a.a}, kr(a){return a.b}, @@ -499,57 +499,57 @@ o.$flags=1 s=o for(o=s.length,r=0;r=0)return a.replace(/\$/g,"$$$$") +mF(a){if(a.indexOf("$",0)>=0)return a.replace(/\$/g,"$$$$") return a}, jM(a){if(/[[\]{}()*+?.\\^$|]/.test(a))return a.replace(/[[\]{}()*+?.\\^$|]/g,"\\$&") return a}, @@ -590,7 +590,7 @@ s=a.length for(r=c,q=0;q=0)return a.split(b).join(c) -return a.replace(new RegExp(A.jM(b),"g"),A.mH(c))}, +return a.replace(new RegExp(A.jM(b),"g"),A.mF(c))}, cy:function cy(a,b){this.a=a this.b=b}, bF:function bF(a,b){this.a=a @@ -652,9 +652,9 @@ _.b=b _.c=c _.d=null _.$ti=d}, -hf:function hf(a){this.a=a}, -hg:function hg(a){this.a=a}, -hh:function hh(a){this.a=a}, +h3:function h3(a){this.a=a}, +h4:function h4(a){this.a=a}, +h5:function h5(a){this.a=a}, aH:function aH(){}, b0:function b0(){}, d7:function d7(a,b){var _=this @@ -662,7 +662,7 @@ _.a=a _.b=b _.e=_.d=_.c=null}, fA:function fA(a){this.b=a}, -n0(a){throw A.C(new A.bx("Field '"+a+"' has been assigned during initialization."),new Error())}, +n3(a){throw A.C(new A.bx("Field '"+a+"' has been assigned during initialization."),new Error())}, D(){throw A.C(A.iI(""),new Error())}, jS(){throw A.C(A.kQ(""),new Error())}, fl(a){var s=new A.fk(a) @@ -684,11 +684,11 @@ return new Uint32Array(a,b,c)}, iP(a){return new Uint8Array(a)}, ar(a,b,c){A.cH(a,b,c) return c==null?new Uint8Array(a,b):new Uint8Array(a,b,c)}, -aL(a,b,c){if(a>>>0!==a||a>=c)throw A.d(A.ha(b,a))}, +aL(a,b,c){if(a>>>0!==a||a>=c)throw A.d(A.fZ(b,a))}, lX(a,b,c){var s if(!(a>>>0!==a))s=b>>>0!==b||a>b||b>c else s=!0 -if(s)throw A.d(A.mG(a,b,c)) +if(s)throw A.d(A.mE(a,b,c)) return b}, aU:function aU(){}, by:function by(){}, @@ -746,7 +746,7 @@ return A.jg(a1,k,i) case 11:h=a2.x g=A.bm(a1,h,a3,a4) f=a2.y -e=A.mv(a1,f,a3,a4) +e=A.mt(a1,f,a3,a4) if(g===h&&e===f)return a2 return A.jd(a1,g,e) case 12:d=a2.y @@ -767,14 +767,14 @@ for(s=!1,r=0;r=p)return A.b(q,0) -s=A.cE(v.typeUniverse,A.i3(q[0]),"@<0>") +s=A.cE(v.typeUniverse,A.i2(q[0]),"@<0>") for(r=1;r=0)p+=" "+r[q];++q}return p+"})"}, -jt(a3,a4,a5){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1=", ",a2=null +js(a3,a4,a5){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1=", ",a2=null if(a5!=null){s=a5.length if(a4==null)a4=A.x([],t.s) else a2=a4.length @@ -998,17 +998,17 @@ if(l===6){s=a.x r=A.Y(s,b) q=s.w return(q===11||q===12?"("+r+")":r)+"?"}if(l===7)return"FutureOr<"+A.Y(a.x,b)+">" -if(l===8){p=A.mx(a.x) +if(l===8){p=A.mv(a.x) o=a.y -return o.length>0?p+("<"+A.jA(o,b)+">"):p}if(l===10)return A.mm(a,b) -if(l===11)return A.jt(a,b,null) -if(l===12)return A.jt(a.x,b,a.y) +return o.length>0?p+("<"+A.jz(o,b)+">"):p}if(l===10)return A.mm(a,b) +if(l===11)return A.js(a,b,null) +if(l===12)return A.js(a.x,b,a.y) if(l===13){n=a.x m=b.length n=m-1-n if(!(n>=0&&n"))}, -h5(a,b){A.mr(new A.h6(a,b))}, -jy(a,b,c,d,e){var s,r=$.w +fU(a,b){A.mp(new A.fV(a,b))}, +jx(a,b,c,d,e){var s,r=$.w if(r===c)return d.$0() $.w=c s=r try{r=d.$0() return r}finally{$.w=s}}, -jz(a,b,c,d,e,f,g){var s,r=$.w +jy(a,b,c,d,e,f,g){var s,r=$.w if(r===c)return d.$1(e) $.w=c s=r try{r=d.$1(e) return r}finally{$.w=s}}, -mq(a,b,c,d,e,f,g,h,i){var s,r=$.w +mo(a,b,c,d,e,f,g,h,i){var s,r=$.w if(r===c)return d.$2(e,f) $.w=c s=r @@ -1593,7 +1593,7 @@ try{r=d.$2(e,f) return r}finally{$.w=s}}, e1(a,b,c,d){t.M.a(d) if(B.d!==c){d=c.cC(d) -d=d}A.jB(d)}, +d=d}A.jA(d)}, fe:function fe(a){this.a=a}, fd:function fd(a,b,c){this.a=a this.b=b @@ -1608,7 +1608,7 @@ this.b=!1 this.$ti=b}, fP:function fP(a){this.a=a}, fQ:function fQ(a){this.a=a}, -h7:function h7(a){this.a=a}, +fW:function fW(a){this.a=a}, ad:function ad(a,b){this.a=a this.b=b}, dI:function dI(){}, @@ -1656,7 +1656,7 @@ this.b=b}, fE:function fE(a,b,c){this.a=a this.b=b this.c=c}, -h6:function h6(a,b){this.a=a +fV:function fV(a,b){this.a=a this.b=b}, d9(a,b){return new A.c3(a.h("@<0>").E(b).h("c3<1,2>"))}, hI(a){var s,r @@ -1978,8 +1978,8 @@ iy(a){return new A.aR(a)}, bX(a){if(typeof a=="number"||A.cI(a)||a==null)return J.b4(a) if(typeof a=="string")return JSON.stringify(a) return A.iT(a)}, -kD(a,b){A.h9(a,"error",t.K) -A.h9(b,"stackTrace",t.l) +kD(a,b){A.fY(a,"error",t.K) +A.fY(b,"stackTrace",t.l) A.kC(a,b)}, cP(a){return new A.cO(a)}, J(a,b){return new A.ah(!1,null,b,a)}, @@ -2148,7 +2148,7 @@ if(typeof a=="function")throw A.d(A.J("Attempting to rewrap a JS function.",null s=function(b,c){return function(d,e,f,g){return b(c,d,e,f,g,arguments.length)}}(A.lU,a) s[$.bQ()]=a return s}, -i0(a){var s +i_(a){var s if(typeof a=="function")throw A.d(A.J("Attempting to rewrap a JS function.",null)) s=function(b,c){return function(d,e,f,g,h){return b(c,d,e,f,g,h,arguments.length)}}(A.lV,a) s[$.bQ()]=a @@ -2182,14 +2182,14 @@ if(g===3)return a.$3(b,c,d) if(g===2)return a.$2(b,c) if(g===1)return a.$1(b) return a.$0()}, -mL(a,b,c){return c.a(a[b])}, -h8(a,b,c,d){return d.a(a[b].apply(a,c))}, +mJ(a,b,c){return c.a(a[b])}, +fX(a,b,c,d){return d.a(a[b].apply(a,c))}, bP(a,b){var s=new A.F($.w,b.h("F<0>")),r=new A.cq(s,b.h("cq<0>")) -a.then(A.bN(new A.hl(r,b),1),A.bN(new A.hm(r),1)) +a.then(A.bN(new A.h9(r,b),1),A.bN(new A.ha(r),1)) return s}, -hl:function hl(a,b){this.a=a +h9:function h9(a,b){this.a=a this.b=b}, -hm:function hm(a){this.a=a}, +ha:function ha(a){this.a=a}, dQ:function dQ(a){this.a=a}, dj:function dj(){}, dx:function dx(){}, @@ -2198,21 +2198,30 @@ this.b=b}, cj:function cj(a,b,c){this.a=a this.b=b this.c=c}, -mX(){var s={},r=v.G,q=new A.F($.w,t.cd) -q.av(null) -s.a=q -r.onmessage=A.bH(new A.ho(s,new A.fO(),r))}, -ho:function ho(a,b,c){this.a=a -this.b=b -this.c=c}, -hn:function hn(a,b,c){this.a=a -this.b=b -this.c=c}, -fO:function fO(){var _=this -_.e=_.d=_.c=_.b=_.a=null}, -mD(a,b,c,d){var s,r,q,p,o,n -A.mn(a) -A.mp(a) +mV(a){var s,r,q +a.by(B.j,!0,B.K,"orm_decimal_avg_v1",t.gB) +a.aT(B.C,!0,new A.hb(),"orm_decimal_div_v1") +a.aT(B.j,!0,new A.hc(),"orm_decimal_round_v1") +a.a7(B.j,!0,!1,new A.hd(),"orm_decimal_cast_v1") +a.a7(B.j,!0,!1,new A.he(),"orm_decimal_fits_v1") +a.bz(new A.hf(),"orm_decimal_v1") +for(s=["add","sub","mul"],r=0;r<3;++r){q=s[r] +a.aT(B.B,!0,new A.hg(q),"orm_decimal_"+q+"_v1")}a.by(B.o,!0,B.L,"orm_decimal_sum_v1",t.aW)}, +hb:function hb(){}, +hc:function hc(){}, +hd:function hd(){}, +he:function he(){}, +hf:function hf(){}, +hg:function hg(a){this.a=a}, +bi:function bi(a){this.a=a +this.c=this.b=0}, +dL:function dL(){}, +bD:function bD(a){this.a=a +this.c=this.b=null}, +dK:function dK(){}, +mB(a,b,c,d){var s,r,q,p,o,n +A.mV(a) +A.mW(a) a.a9("PRAGMA foreign_keys = ON") s=a.ap("PRAGMA foreign_keys") if(!J.ab(B.b.ga_(s.ga_(s).b),1))throw A.d(B.an) @@ -2236,47 +2245,26 @@ _.a=a _.b=b _.c=!1 _.d=c}, -mn(a){var s,r,q -a.by(B.j,!0,B.K,"orm_decimal_avg_v1",t.gB) -a.aT(B.C,!0,new A.fU(),"orm_decimal_div_v1") -a.aT(B.j,!0,new A.fV(),"orm_decimal_round_v1") -a.a7(B.j,!0,!1,new A.fW(),"orm_decimal_cast_v1") -a.a7(B.j,!0,!1,new A.fX(),"orm_decimal_fits_v1") -a.bz(new A.fY(),"orm_decimal_v1") -for(s=["add","sub","mul"],r=0;r<3;++r){q=s[r] -a.aT(B.B,!0,new A.fZ(q),"orm_decimal_"+q+"_v1")}a.by(B.o,!0,B.L,"orm_decimal_sum_v1",t.aW)}, jN(a){var s,r,q,p=[] for(s=a.length,r=0;r(1)").a(new A.hq()),s),s.h("V.E")) return A.x([r,q,a.c],t.f)}, hp:function hp(){}, hq:function hq(){}, +mX(){var s={},r=v.G,q=new A.F($.w,t.cd) +q.av(null) +s.a=q +r.onmessage=A.bH(new A.ho(s,new A.fO(),r))}, +ho:function ho(a,b,c){this.a=a +this.b=b +this.c=c}, +hn:function hn(a,b,c){this.a=a +this.b=b +this.c=c}, +fO:function fO(){var _=this +_.e=_.d=_.c=_.b=_.a=null}, ky(a){var s A:{if(a instanceof A.aj){s=a break A}if(typeof a=="string"){s=A.eC(a) @@ -2349,6 +2349,12 @@ break A}if(A.bJ(a)){s=A.bt(A.O(a),0) break A}if(a instanceof A.z){s=A.bt(a,0) break A}s=A.u(B.a7)}return s}, iQ(a,b,c){return new A.a0(a,b)}, +ci:function ci(a){this.a=a}, +hy:function hy(a,b,c){this.c=a +this.d=b +this.$ti=c}, +a0:function a0(a,b){this.a=a +this.b=b}, kB(a,b){return new A.az(a,b)}, bt(a,b){var s=a.m(0,$.E()) if(s===0)return $.ib() @@ -2414,112 +2420,17 @@ o=(o[0]&1)===0}else o=!0 o=!o}}else o=!0 break default:o=null}if(o)q=q.S(0,A.O(s))}return A.bt(q,c)}, -hY(a){var s -A:{if(a instanceof A.a3){s=A.fR(a) -break A}if(typeof a=="string"){s=A.ml(a) -break A}s=A.u(B.a4)}return s}, -fR(a){var s=a.dt(),r=a.a,q=!0 -if(r>=-2108668032e5)if(r<=864e13)q=r===864e13&&s.b!==0 -if(q)throw A.d(B.w) -return s}, -jq(a){var s=A.fR(t.k.a(a)),r=A.hG(A.eO(s),A.eM(s),A.eJ(s)),q=A.eB(A.eK(s),A.eL(s),A.eN(s),A.hJ(s)*1000+s.b),p=r.gaB(),o=q.i(0),n=r.a<=0?" BC":"" -return p+" "+o+"+00"+n}, -ml(a3){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0=1e6,a1=864e8,a2=$.ki().a3(a3) -if(a2==null||a2.ga8()!==a3.length)throw A.d(B.a_) -try{e=a2.b -if(1>=e.length)return A.b(e,1) -e=e[1] -e.toString -s=A.aa(e) -e=a2.b -if(6>=e.length)return A.b(e,6) -if(e[6]!=null){e=s -if(typeof e!=="number")return e.e9() -if(e<=0)throw A.d(B.V) -e=s -if(typeof e!=="number")return A.he(e) -s=1-e}e=a2.b -if(2>=e.length)return A.b(e,2) -e=e[2] -e.toString -r=A.aa(e) -e=a2.b -if(3>=e.length)return A.b(e,3) -e=e[3] -e.toString -q=A.aa(e) -e=r -if(typeof e!=="number")return e.b1() -d=!0 -if(!(e<1)){e=r -if(typeof e!=="number")return e.a5() -if(!(e>12)){e=q -if(typeof e!=="number")return e.b1() -if(!(e<1)){e=q -d=A.iN(s,r) -if(typeof e!=="number")return e.a5() -d=e>d -e=d}else e=d}else e=d}else e=d -if(e)throw A.d(B.T) -e=a2.b -if(5>=e.length)return A.b(e,5) -p=e[5] -o=0 -if(p!=null&&p.toUpperCase()!=="Z"){e=B.c.b8(p,1) -n=A.mZ(e,":","") -m=A.aa(J.hv(n,0,2)) -l=J.ac(n)>=4?A.aa(J.hv(n,2,4)):0 -k=J.ac(n)===6?A.aa(J.hv(n,4,6)):0 -e=m -if(typeof e!=="number")return e.a5() -d=!0 -if(!(e>15)){e=l -if(typeof e!=="number")return e.a5() -if(!(e>59)){e=k -if(typeof e!=="number")return e.a5() -e=e>59}else e=d}else e=d -if(e)throw A.d(B.a0) -e=m -if(typeof e!=="number")return e.q() -d=l -if(typeof d!=="number")return A.he(d) -c=k -if(typeof c!=="number")return A.he(c) -b=p -if(0>=b.length)return A.b(b,0) -b=b[0]==="-"?-1:1 -o=((e*60+d)*60+c)*1e6*b}e=a2.b -if(4>=e.length)return A.b(e,4) -e=e[4] -e.toString -e=A.eC(e) -d=o -if(typeof d!=="number")return A.he(d) -j=e.a-d -i=A.ju(s,r,q)+A.fS(j,a1) -d=j -if(typeof d!=="number")return d.n() -h=B.l.n(d,a1) -e=i -if(typeof e!=="number")return e.b1() -d=!0 -if(!(e<0)){e=i -if(typeof e!=="number")return e.a5() -if(!(e>102440588))e=J.ab(i,102440588)&&!J.ab(h,0) -else e=d}else e=d -if(e)throw A.d(B.w) -g=A.iL(i) -f=A.hH(h) -e=A.it(g.a,g.b,g.c,B.a.l(f.a,36e8),B.a.n(B.a.l(f.a,6e7),60),B.a.n(B.a.l(f.a,a0),60),B.a.l(B.a.n(f.a,a0),1000),B.a.n(B.a.n(f.a,a0),1000)) -return e}catch(a){if(t.G.b(A.I(a)))throw A.d(B.S) -else throw a}}, +aA:function aA(a,b){this.a=a +this.b=b}, +az:function az(a,b){this.a=a +this.b=b}, fS(a,b){var s=B.a.ad(a,b) return s-(a<0&&B.a.n(a,b)!==0?1:0)}, -ju(a,b,c){var s=a-(b<=2?1:0),r=A.fS(s,400),q=s-r*400,p=b>2?-3:9 +jt(a,b,c){var s=a-(b<=2?1:0),r=A.fS(s,400),q=s-r*400,p=b>2?-3:9 return r*146097+q*365+B.a.l(q,4)-B.a.l(q,100)+B.a.l(153*(b+p)+2,5)+c-1+1721120}, hG(a,b,c){var s if(a<-4713||a>5874897||b<1||b>12||c<1||c>A.iN(a,b))throw A.d(A.aV("Invalid Gregorian date.")) -s=A.ju(a,b,c) +s=A.jt(a,b,c) if(s<0||s>2147483493)throw A.d(A.aV("Date exceeds the finite PostgreSQL DATE range.")) return new A.ap(a,b,c,s)}, iL(a){var s,r,q,p,o,n,m,l,k @@ -2616,7 +2527,7 @@ return s}catch(r){s=A.I(r) if(s instanceof A.v)return null else if(t.G.b(s))return null else throw r}}, -jC(a,b,c){var s,r,q +jB(a,b,c){var s,r,q if(b<0||b>6)A.u(A.P(b,0,6,"digits",null)) if(!(b>=0&&b<7))return A.b(B.x,b) s=B.x[b] @@ -2625,16 +2536,105 @@ q=r*2 if(q<=s)q=q===s&&!c else q=!0 return q?s-r:-r}, -ci:function ci(a){this.a=a}, -hy:function hy(a,b,c){this.c=a -this.d=b -this.$ti=c}, -a0:function a0(a,b){this.a=a -this.b=b}, -aA:function aA(a,b){this.a=a -this.b=b}, -az:function az(a,b){this.a=a -this.b=b}, +i6(a){var s +A:{if(a instanceof A.a3){s=A.fR(a) +break A}if(typeof a=="string"){s=A.ml(a) +break A}s=A.u(B.a4)}return s}, +fR(a){var s=a.dt(),r=a.a,q=!0 +if(r>=-2108668032e5)if(r<=864e13)q=r===864e13&&s.b!==0 +if(q)throw A.d(B.w) +return s}, +jF(a){var s=A.fR(t.k.a(a)),r=A.hG(A.eO(s),A.eM(s),A.eJ(s)),q=A.eB(A.eK(s),A.eL(s),A.eN(s),A.hJ(s)*1000+s.b),p=r.gaB(),o=q.i(0),n=r.a<=0?" BC":"" +return p+" "+o+"+00"+n}, +ml(a3){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0=1e6,a1=864e8,a2=$.ki().a3(a3) +if(a2==null||a2.ga8()!==a3.length)throw A.d(B.a_) +try{e=a2.b +if(1>=e.length)return A.b(e,1) +e=e[1] +e.toString +s=A.aa(e) +e=a2.b +if(6>=e.length)return A.b(e,6) +if(e[6]!=null){e=s +if(typeof e!=="number")return e.e9() +if(e<=0)throw A.d(B.V) +e=s +if(typeof e!=="number")return A.h2(e) +s=1-e}e=a2.b +if(2>=e.length)return A.b(e,2) +e=e[2] +e.toString +r=A.aa(e) +e=a2.b +if(3>=e.length)return A.b(e,3) +e=e[3] +e.toString +q=A.aa(e) +e=r +if(typeof e!=="number")return e.b1() +d=!0 +if(!(e<1)){e=r +if(typeof e!=="number")return e.a5() +if(!(e>12)){e=q +if(typeof e!=="number")return e.b1() +if(!(e<1)){e=q +d=A.iN(s,r) +if(typeof e!=="number")return e.a5() +d=e>d +e=d}else e=d}else e=d}else e=d +if(e)throw A.d(B.T) +e=a2.b +if(5>=e.length)return A.b(e,5) +p=e[5] +o=0 +if(p!=null&&p.toUpperCase()!=="Z"){e=B.c.b8(p,1) +n=A.mZ(e,":","") +m=A.aa(J.hv(n,0,2)) +l=J.ac(n)>=4?A.aa(J.hv(n,2,4)):0 +k=J.ac(n)===6?A.aa(J.hv(n,4,6)):0 +e=m +if(typeof e!=="number")return e.a5() +d=!0 +if(!(e>15)){e=l +if(typeof e!=="number")return e.a5() +if(!(e>59)){e=k +if(typeof e!=="number")return e.a5() +e=e>59}else e=d}else e=d +if(e)throw A.d(B.a0) +e=m +if(typeof e!=="number")return e.q() +d=l +if(typeof d!=="number")return A.h2(d) +c=k +if(typeof c!=="number")return A.h2(c) +b=p +if(0>=b.length)return A.b(b,0) +b=b[0]==="-"?-1:1 +o=((e*60+d)*60+c)*1e6*b}e=a2.b +if(4>=e.length)return A.b(e,4) +e=e[4] +e.toString +e=A.eC(e) +d=o +if(typeof d!=="number")return A.h2(d) +j=e.a-d +i=A.jt(s,r,q)+A.fS(j,a1) +d=j +if(typeof d!=="number")return d.n() +h=B.l.n(d,a1) +e=i +if(typeof e!=="number")return e.b1() +d=!0 +if(!(e<0)){e=i +if(typeof e!=="number")return e.a5() +if(!(e>102440588))e=J.ab(i,102440588)&&!J.ab(h,0) +else e=d}else e=d +if(e)throw A.d(B.w) +g=A.iL(i) +f=A.hH(h) +e=A.it(g.a,g.b,g.c,B.a.l(f.a,36e8),B.a.n(B.a.l(f.a,6e7),60),B.a.n(B.a.l(f.a,a0),60),B.a.l(B.a.n(f.a,a0),1000),B.a.n(B.a.n(f.a,a0),1000)) +return e}catch(a){if(t.G.b(A.I(a)))throw A.d(B.S) +else throw a}}, ap:function ap(a,b,c,d){var _=this _.a=a _.b=b @@ -2656,13 +2656,13 @@ eY:function eY(){}, ao:function ao(a,b){this.a=a this.$ti=b}, b5:function b5(a){this.a=a}, -hZ(a,b,c){var s,r,q=new A.dz(c,A.iJ(c.b,null,!1,t.X)) -try{A.i_(a,b.$1(q))}catch(r){s=A.I(r) +hY(a,b,c){var s,r,q=new A.dz(c,A.iJ(c.b,null,!1,t.X)) +try{A.hZ(a,b.$1(q))}catch(r){s=A.I(r) a.b6(A.bX(s))}finally{}}, -js(a,b){var s,r -try{A.i_(a,b.$0())}catch(r){s=A.I(r) +jr(a,b){var s,r +try{A.hZ(a,b.$0())}catch(r){s=A.I(r) a.b6(A.bX(s))}}, -i_(a,b){var s,r,q,p +hZ(a,b){var s,r,q,p A:{s=null if(b==null){a.a.d.sqlite3_result_null(a.b) break A}if(A.bJ(b)){a.a.d.sqlite3_result_int64(a.b,t.C.a(v.G.BigInt(A.O(b).i(0)))) @@ -2682,7 +2682,7 @@ p=q.a2(b) q=q.d q.sqlite3_result_blob64(a.b,p,t.C.a(v.G.BigInt(J.ac(b))),-1) q.dart_sqlite3_free(p) -break A}if(t.u.b(b)){A.i_(a,b.a) +break A}if(t.u.b(b)){A.hZ(a,b.a) a.a.d.sqlite3_result_subtype(a.b,b.b) break A}s=A.u(A.ax(b,"result","Unsupported type"))}return s}, cY:function cY(a,b,c){var _=this @@ -2741,8 +2741,8 @@ _.a=a _.b=b _.c=c _.d=0}, -mV(a){return new A.cp(A.x(A.A(A.B(new v.G.URL(a,"file:///")).pathname).split("/"),t.s),t.bB.a(new A.hk()),t.cc)}, -hk:function hk(){}, +mT(a){return new A.cp(A.x(A.A(A.B(new v.G.URL(a,"file:///")).pathname).split("/"),t.s),t.bB.a(new A.h8()),t.cc)}, +h8:function h8(){}, bs:function bs(){}, c_:function c_(){}, dq:function dq(a,b,c){this.d=a @@ -2880,7 +2880,7 @@ p=t.m s=3 return A.R(A.bP(A.B(j.getDirectory()),p),$async$ds) case 3:o=d -n=A.mV(a),m=J.cN(n.a),n=new A.bh(m,n.b,n.$ti.h("bh<1>")),l=null +n=A.mT(a),m=J.cN(n.a),n=new A.bh(m,n.b,n.$ti.h("bh<1>")),l=null case 4:if(!n.p()){s=6 break}s=7 return A.R(A.bP(A.B(o.getDirectoryHandle(m.gv(),{create:!0})),p),$async$ds) @@ -2977,18 +2977,18 @@ dP:function dP(){}, bf:function bf(a,b){this.a=a this.b=b}, jT(a){return v.mangledGlobalNames[a]}, -mW(a){if(typeof dartPrint=="function"){dartPrint(a) +mU(a){if(typeof dartPrint=="function"){dartPrint(a) return}if(typeof console=="object"&&typeof console.log!="undefined"){console.log(a) return}if(typeof print=="function"){print(a) return}throw"Unable to print message: "+String(a)}, kO(a,b,c,d,e,f){var s=a[b](c,d,e) return s}, -i6(a,b,c,d,e,f){var s,r,q=b.a,p=b.b,o=q.d,n=A.c(o.sqlite3_extended_errcode(p)),m=A.c(o.sqlite3_error_offset(p)) +i5(a,b,c,d,e,f){var s,r,q=b.a,p=b.b,o=q.d,n=A.c(o.sqlite3_extended_errcode(p)),m=A.c(o.sqlite3_error_offset(p)) A:{if(m<0){s=null break A}s=m break A}r=a.a return new A.ck(A.av(q.b,A.c(o.sqlite3_errmsg(p)),null),A.av(r.b,A.c(r.d.sqlite3_errstr(n)),null)+" (code "+n+")",c,s,d,e,f)}, -cM(a,b,c,d,e){throw A.d(A.i6(a.a,a.b,b,c,d,e))}, +cM(a,b,c,d,e){throw A.d(A.i5(a.a,a.b,b,c,d,e))}, il(a){if(a.m(0,$.jW())<0||a.m(0,$.jV())>0)throw A.d(A.iz("BigInt value exceeds the range of 64 bits")) return a}, i7(a,b,c){var s=a?2049:1 @@ -3015,7 +3015,7 @@ iA(a,b,c){var s=A.c(a.read(b,c)) return s}, iB(a,b,c){var s=A.c(a.write(b,c)) return s}, -mT(){return A.mX()}},B={} +mR(){return A.mX()}},B={} var w=[A,J,B] var $={} A.hC.prototype={} @@ -3023,7 +3023,7 @@ J.d3.prototype={ I(a,b){return a===b}, gu(a){return A.dm(a)}, i(a){return"Instance of '"+A.dn(a)+"'"}, -gF(a){return A.bn(A.i1(this))}} +gF(a){return A.bn(A.i0(this))}} J.d5.prototype={ i(a){return String(a)}, gu(a){return a?519018:218159}, @@ -3080,11 +3080,11 @@ i(a){return A.iF(a,"[","]")}, gG(a){return new J.bR(a,a.length,A.am(a).h("bR<1>"))}, gu(a){return A.dm(a)}, gk(a){return a.length}, -j(a,b){if(!(b>=0&&b=0&&b=0&&b=0&&b=-2147483648&&s<=2147483647)return s|0 if(s>0){if(s!==1/0)return Math.floor(s)}else if(s>-1/0)return Math.ceil(s) throw A.d(A.aY("Result of truncating division is "+A.n(s)+": "+A.n(a)+" ~/ "+b))}, -O(a,b){if(b<0)throw A.d(A.i4(b)) +O(a,b){if(b<0)throw A.d(A.i3(b)) return b>31?0:a<>>0}, Z(a,b){var s -if(b<0)throw A.d(A.i4(b)) +if(b<0)throw A.d(A.i3(b)) if(a>0)s=this.bm(a,b) else{s=b>31?31:b s=a>>s>>>0}return s}, @@ -3514,13 +3514,13 @@ return!1}else{r.d=s.b r.c=s.c return!0}}, $iK:1} -A.hf.prototype={ +A.h3.prototype={ $1(a){return this.a(a)}, $S:41} -A.hg.prototype={ +A.h4.prototype={ $2(a,b){return this.a(a,b)}, $S:33} -A.hh.prototype={ +A.h5.prototype={ $1(a){return this.a(A.A(a))}, $S:43} A.aH.prototype={ @@ -3711,7 +3711,7 @@ this.a.a=t.M.a(a) s=this.b r=this.c s.firstChild?s.removeChild(r):s.appendChild(r)}, -$S:37} +$S:30} A.ff.prototype={ $0(){this.a.$0()}, $S:14} @@ -3741,7 +3741,7 @@ $S:6} A.fQ.prototype={ $2(a,b){this.a.$2(1,new A.bY(a,t.l.a(b)))}, $S:45} -A.h7.prototype={ +A.fW.prototype={ $2(a,b){this.a(A.c(a),b)}, $S:48} A.ad.prototype={ @@ -3773,7 +3773,7 @@ b0(a,b,c){var s,r,q,p=this.$ti p.E(c).h("1/(2)").a(a) s=$.w if(s===B.d){if(b!=null&&!t.R.b(b)&&!t.x.b(b))throw A.d(A.ax(b,"onError",u.c))}else{c.h("@<0/>").E(p.c).h("1(2)").a(a) -if(b!=null)b=A.mo(b,s)}r=new A.F(s,c.h("F<0>")) +if(b!=null)b=A.mn(b,s)}r=new A.F(s,c.h("F<0>")) q=b==null?1:3 this.au(new A.bj(r,q,a,b,p.h("@<1>").E(c).h("bj<1,2>"))) return r}, @@ -3923,30 +3923,30 @@ A.dV.prototype={ dq(a){var s,r,q t.M.a(a) try{if(B.d===$.w){a.$0() -return}A.jy(null,null,this,a,t.H)}catch(q){s=A.I(q) +return}A.jx(null,null,this,a,t.H)}catch(q){s=A.I(q) r=A.bp(q) -A.h5(A.bl(s),t.l.a(r))}}, +A.fU(A.bl(s),t.l.a(r))}}, dr(a,b,c){var s,r,q c.h("~(0)").a(a) c.a(b) try{if(B.d===$.w){a.$1(b) -return}A.jz(null,null,this,a,b,t.H,c)}catch(q){s=A.I(q) +return}A.jy(null,null,this,a,b,t.H,c)}catch(q){s=A.I(q) r=A.bp(q) -A.h5(A.bl(s),t.l.a(r))}}, +A.fU(A.bl(s),t.l.a(r))}}, cC(a){return new A.fD(this,t.M.a(a))}, cD(a,b){return new A.fE(this,b.h("~(0)").a(a),b)}, dm(a,b){b.h("0()").a(a) if($.w===B.d)return a.$0() -return A.jy(null,null,this,a,b)}, +return A.jx(null,null,this,a,b)}, b_(a,b,c,d){c.h("@<0>").E(d).h("1(2)").a(a) d.a(b) if($.w===B.d)return a.$1(b) -return A.jz(null,null,this,a,b,c,d)}, +return A.jy(null,null,this,a,b,c,d)}, dn(a,b,c,d,e,f){d.h("@<0>").E(e).E(f).h("1(2,3)").a(a) e.a(b) f.a(c) if($.w===B.d)return a.$2(b,c) -return A.mq(null,null,this,a,b,c,d,e,f)}, +return A.mo(null,null,this,a,b,c,d,e,f)}, bM(a,b,c,d){return b.h("@<0>").E(c).E(d).h("1(2,3)").a(a)}} A.fD.prototype={ $0(){return this.a.dq(this.b)}, @@ -3955,7 +3955,7 @@ A.fE.prototype={ $1(a){var s=this.c return this.a.dr(this.b,s.a(a),s)}, $S(){return this.c.h("~(0)")}} -A.h6.prototype={ +A.fV.prototype={ $0(){A.kD(this.a,this.b)}, $S:0} A.k.prototype={ @@ -4008,7 +4008,7 @@ s=A.n(a) r.a=(r.a+=s)+": " s=A.n(b) r.a+=s}, -$S:22} +$S:37} A.fL.prototype={ $0(){var s,r try{s=new TextDecoder("utf-8",{fatal:true}) @@ -4549,7 +4549,7 @@ A.e.prototype={$ie:1, I(a,b){return this===b}, gu(a){return A.dm(this)}, i(a){return"Instance of '"+A.dn(this)+"'"}, -gF(a){return A.mM(this)}, +gF(a){return A.mK(this)}, toString(){return this.i(this)}} A.e_.prototype={ i(a){return""}, @@ -4562,10 +4562,10 @@ A.d0.prototype={ i(a){return"Expando:null"}} A.eE.prototype={ i(a){return"Promise was rejected with a value of `"+(this.a?"undefined":"null")+"`."}} -A.hl.prototype={ +A.h9.prototype={ $1(a){return this.a.aQ(this.b.h("0/?").a(a))}, $S:6} -A.hm.prototype={ +A.ha.prototype={ $1(a){if(a==null)return this.a.bx(new A.eE(a===undefined)) return this.a.bx(a)}, $S:6} @@ -4593,12 +4593,258 @@ A.dj.prototype={} A.dx.prototype={} A.eV.prototype={} A.cj.prototype={} +A.hb.prototype={ +$1(a){var s,r,q,p,o,n,m +if(a.j(0,0)==null||a.j(0,1)==null)s=null +else{s=A.ay(a.j(0,0)) +r=A.ay(a.j(0,1)) +q=A.c(a.j(0,2)) +p=A.c(a.j(0,3)) +if(!(p>=0&&p<6))return A.b(B.i,p) +p=B.i[p] +t.U.a(r) +A.hB(q) +o=r.a +n=o.m(0,$.E()) +if(n===0)A.u(A.aY("Division by zero")) +m=r.b-s.b+q +s=s.a +s=s.q(0,m>0?A.O(10).J(m):$.Z()) +s=A.hz(s,o.q(0,m<0?A.O(10).J(-m):$.Z()),q,p).i(0)}return s}, +$S:3} +A.hc.prototype={ +$1(a){var s,r,q +if(a.j(0,0)==null)s=null +else{s=A.ay(a.j(0,0)) +r=A.c(a.j(0,1)) +q=A.c(a.j(0,2)) +if(!(q>=0&&q<6))return A.b(B.i,q) +q=s.bO(r,B.i[q]).i(0) +s=q}return s}, +$S:3} +A.hd.prototype={ +$1(a){var s,r,q,p +if(a.j(0,0)==null)s=null +else{s=A.ay(a.j(0,0)) +r=A.c(a.j(0,1)) +q=A.c(a.j(0,2)) +A.ix(r,q) +p=s.bO(q,B.t) +if(!p.bG(r,q))A.u(A.aV("Decimal does not fit NUMERIC("+r+", "+q+").")) +s=p.i(0)}return s}, +$S:3} +A.he.prototype={ +$1(a){var s +if(typeof a.j(0,0)=="string"){s=A.hA(A.A(a.j(0,0))) +s=s==null?null:s.bG(A.c(a.j(0,1)),A.c(a.j(0,2))) +s=s===!0}else s=!1 +return s?1:0}, +$S:31} +A.hf.prototype={ +$2(a,b){var s,r,q +if(a==b)return 0 +if(a==null)return-1 +if(b==null)return 1 +s=A.hA(a) +r=A.hA(b) +q=s!=null +if(q&&r!=null)return s.m(0,r) +if(q)return-1 +if(r!=null)return 1 +return B.c.m(a,b)}, +$S:12} +A.hg.prototype={ +$1(a){var s,r,q,p +if(a.j(0,0)==null||a.j(0,1)==null)return null +s=A.ay(a.j(0,0)) +r=A.ay(a.j(0,1)) +q=this.a +A:{if("add"===q){p=s.S(0,r) +break A}if("sub"===q){p=s.S(0,new A.az(r.a.D(0),r.b)) +break A}p=A.bt(s.a.q(0,r.a),s.b+r.b) +break A}return p.i(0)}, +$S:3} +A.bi.prototype={ +ai(a,b,c){var s,r=this +if(r.c===0){r.a=b.a +r.b=b.b}else{s=b.b +if(s>r.b){r.a=r.a.q(0,A.O(10).J(s-r.b)) +r.b=s}r.a=r.a.S(0,b.a.q(0,A.O(c)).q(0,A.O(10).J(r.b-s)))}if((r.c+=c)===0){r.a=$.E() +r.b=0}}, +an(){return this.c===0?null:A.bt(this.a,this.b).i(0)}} +A.dL.prototype={ +aS(){return new A.ao(new A.bi($.E()),t.y)}, +b7(a,b){t.y.a(b) +if(a.j(0,0)==null)return +b.a.ai(0,A.ay(a.j(0,0)),1)}, +bI(a,b){t.y.a(b) +if(a.j(0,0)==null)return +b.a.ai(0,A.ay(a.j(0,0)),-1)}, +bS(a){var s=t.y.a(a).a +return s.c===0?null:A.bt(s.a,s.b).i(0)}, +bF(a){return t.y.a(a).a.an()}, +$ie3:1} +A.bD.prototype={ +an(){var s,r,q,p,o,n=this.a +if(n.c===0)return null +s=A.O(10).J(Math.abs(n.b)) +r=n.a +r=r.q(0,n.b<0?s:$.Z()) +q=A.O(n.c) +q=q.q(0,n.b>0?s:$.Z()) +p=this.b +p.toString +o=this.c +o.toString +A.hB(p) +n=q.m(0,$.E()) +if(n===0)A.u(A.aY("Division by zero")) +n=r.q(0,p>0?A.O(10).J(p):$.Z()) +return A.hz(n,q.q(0,p<0?A.O(10).J(-p):$.Z()),p,o).i(0)}} +A.dK.prototype={ +aS(){return new A.ao(new A.bD(new A.bi($.E())),t._)}, +b7(a,b){var s,r=t._.a(b).a,q=A.c(a.j(0,1)),p=A.c(a.j(0,2)) +if(!(p>=0&&p<6))return A.b(B.i,p) +s=B.i[p] +p=r.b +if(p!=null)p=p!==q||r.c!==s +else p=!1 +if(p)A.u(A.J("Average scale and rounding must be constant.",null)) +r.b=q +r.c=s +if(a.j(0,0)!=null)r.a.ai(0,A.ay(a.j(0,0)),1)}, +bI(a,b){t._.a(b) +if(a.j(0,0)!=null)b.a.a.ai(0,A.ay(a.j(0,0)),-1)}, +bS(a){return t._.a(a).a.an()}, +bF(a){return t._.a(a).a.an()}, +$ie3:1} +A.eZ.prototype={ +a9(a){var s,r,q,p,o,n,m,l,k,j=this.a,i=j.al(a.a,!0) +try{s=i.b2(new A.b7(A.jN(a.b))) +r=A.x([],t.D) +for(p=s,o=p.d,n=t.X,m=-1;++m,m"));r.p();)r.d.C() +if(s.a>0){s.b=s.c=s.d=s.e=s.f=null +s.a=0 +s.aK()}q.a.C()}} +A.dJ.prototype={ +d4(a){var s,r,q,p=this +if(p.c)return new A.cj(B.ab,p.d,0) +s=A.x([],t.D) +for(r=p.b;s.length"),"T","call") +this.a.bz(new A.hl(c.h("0?(h)").a(b)),"orm_"+a+"_v1")}, +$2(a,b){return this.$1$2(a,b,t.q)}, +$S:49} +A.hl.prototype={ +$2(a,b){var s,r,q +if(a==b)return 0 +if(a==null)return-1 +if(b==null)return 1 +s=this.a +r=s.$1(a) +q=s.$1(b) +s=r!=null +if(s&&q!=null)return J.km(r,q) +if(s)return-1 +if(q!=null)return 1 +return B.c.m(a,b)}, +$S:12} +A.hj.prototype={ +$1(a){var s,r +try{s=A.i6(a) +return s}catch(r){if(A.I(r) instanceof A.v)return null +else throw r}}, +$S:22} +A.hp.prototype={ +$1(a){return A.A(a)}, +$S:23} +A.hq.prototype={ +$1(a){var s=J.ko(t.W.a(a),A.n6(),t.X) +s=A.da(s,s.$ti.h("V.E")) +return s}, +$S:24} A.ho.prototype={ $1(a){var s A.B(a) s=this.a s.a=s.a.ds(new A.hn(a,this.b,this.c),t.H)}, -$S:30} +$S:25} A.hn.prototype={ $1(a){var s=0,r=A.aM(t.H),q=1,p=[],o=this,n,m,l,k,j,i,h,g,f,e var $async$$1=A.aN(function(b,c){if(b===1){p.push(c) @@ -4639,11 +4885,11 @@ break case 5:return A.aJ(null,r) case 1:return A.aI(p.at(-1),r)}}) return A.aK($async$$1,r)}, -$S:31} +$S:21} A.fO.prototype={ W(a6,a7){var s=0,r=A.aM(t.X),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5 var $async$W=A.aN(function(a8,a9){if(a8===1){o.push(a9) -s=p}for(;;)A:switch(s){case 0:if(a6==="hello"){q=A.x([1,"54c9f756d7c963bdcf116d51a4a5380bdc43704ebdef02add66417747c9d76eb"],t.f) +s=p}for(;;)A:switch(s){case 0:if(a6==="hello"){q=A.x([1,"811c9351ca7a8a06979b43b9b68aaf97fac2c0c721c506e574d7897eed0693d6"],t.f) s=1 break}s=a6==="open"?3:4 break @@ -4664,7 +4910,7 @@ d.toString s=13 return A.R(A.bP(A.B(v.G.fetch(l,{integrity:A.A(d)})),t.m),$async$W) case 13:j=a9 -if(!A.e0(j.ok)){d=A.at("HTTP "+A.n(A.mL(j,"status",t.S))) +if(!A.e0(j.ok)){d=A.at("HTTP "+A.n(A.mJ(j,"status",t.S))) throw A.d(d)}p=6 s=12 break @@ -4709,7 +4955,7 @@ n.b=a0 f=a0 n.e=new A.eZ(f,A.d9(t.S,t.al)) d=J.ab(k,"memory")?"memory":"delete" -e=A.mD(f,B.R,!0,d) +e=A.mB(f,B.R,!0,d) d=g.a.a a1=d.b d=d.d @@ -4785,252 +5031,6 @@ p.c.close() p.d.close()}s.d=null}r=q.d if(r!=null){p=q.a if(p!=null)p.a.bR(r)}q.d=q.c=null}} -A.eZ.prototype={ -a9(a){var s,r,q,p,o,n,m,l,k,j=this.a,i=j.al(a.a,!0) -try{s=i.b2(new A.b7(A.jN(a.b))) -r=A.x([],t.D) -for(p=s,o=p.d,n=t.X,m=-1;++m,m"));r.p();)r.d.C() -if(s.a>0){s.b=s.c=s.d=s.e=s.f=null -s.a=0 -s.aK()}q.a.C()}} -A.dJ.prototype={ -d4(a){var s,r,q,p=this -if(p.c)return new A.cj(B.ab,p.d,0) -s=A.x([],t.D) -for(r=p.b;s.length=0&&p<6))return A.b(B.i,p) -p=B.i[p] -t.U.a(r) -A.hB(q) -o=r.a -n=o.m(0,$.E()) -if(n===0)A.u(A.aY("Division by zero")) -m=r.b-s.b+q -s=s.a -s=s.q(0,m>0?A.O(10).J(m):$.Z()) -s=A.hz(s,o.q(0,m<0?A.O(10).J(-m):$.Z()),q,p).i(0)}return s}, -$S:3} -A.fV.prototype={ -$1(a){var s,r,q -if(a.j(0,0)==null)s=null -else{s=A.ay(a.j(0,0)) -r=A.c(a.j(0,1)) -q=A.c(a.j(0,2)) -if(!(q>=0&&q<6))return A.b(B.i,q) -q=s.bO(r,B.i[q]).i(0) -s=q}return s}, -$S:3} -A.fW.prototype={ -$1(a){var s,r,q,p -if(a.j(0,0)==null)s=null -else{s=A.ay(a.j(0,0)) -r=A.c(a.j(0,1)) -q=A.c(a.j(0,2)) -A.ix(r,q) -p=s.bO(q,B.t) -if(!p.bG(r,q))A.u(A.aV("Decimal does not fit NUMERIC("+r+", "+q+").")) -s=p.i(0)}return s}, -$S:3} -A.fX.prototype={ -$1(a){var s -if(typeof a.j(0,0)=="string"){s=A.hA(A.A(a.j(0,0))) -s=s==null?null:s.bG(A.c(a.j(0,1)),A.c(a.j(0,2))) -s=s===!0}else s=!1 -return s?1:0}, -$S:39} -A.fY.prototype={ -$2(a,b){var s,r,q -if(a==b)return 0 -if(a==null)return-1 -if(b==null)return 1 -s=A.hA(a) -r=A.hA(b) -q=s!=null -if(q&&r!=null)return s.m(0,r) -if(q)return-1 -if(r!=null)return 1 -return B.c.m(a,b)}, -$S:16} -A.fZ.prototype={ -$1(a){var s,r,q,p -if(a.j(0,0)==null||a.j(0,1)==null)return null -s=A.ay(a.j(0,0)) -r=A.ay(a.j(0,1)) -q=this.a -A:{if("add"===q){p=s.S(0,r) -break A}if("sub"===q){p=s.S(0,new A.az(r.a.D(0),r.b)) -break A}p=A.bt(s.a.q(0,r.a),s.b+r.b) -break A}return p.i(0)}, -$S:3} -A.bi.prototype={ -ai(a,b,c){var s,r=this -if(r.c===0){r.a=b.a -r.b=b.b}else{s=b.b -if(s>r.b){r.a=r.a.q(0,A.O(10).J(s-r.b)) -r.b=s}r.a=r.a.S(0,b.a.q(0,A.O(c)).q(0,A.O(10).J(r.b-s)))}if((r.c+=c)===0){r.a=$.E() -r.b=0}}, -an(){return this.c===0?null:A.bt(this.a,this.b).i(0)}} -A.dL.prototype={ -aS(){return new A.ao(new A.bi($.E()),t.y)}, -b7(a,b){t.y.a(b) -if(a.j(0,0)==null)return -b.a.ai(0,A.ay(a.j(0,0)),1)}, -bI(a,b){t.y.a(b) -if(a.j(0,0)==null)return -b.a.ai(0,A.ay(a.j(0,0)),-1)}, -bS(a){var s=t.y.a(a).a -return s.c===0?null:A.bt(s.a,s.b).i(0)}, -bF(a){return t.y.a(a).a.an()}, -$ie3:1} -A.bD.prototype={ -an(){var s,r,q,p,o,n=this.a -if(n.c===0)return null -s=A.O(10).J(Math.abs(n.b)) -r=n.a -r=r.q(0,n.b<0?s:$.Z()) -q=A.O(n.c) -q=q.q(0,n.b>0?s:$.Z()) -p=this.b -p.toString -o=this.c -o.toString -A.hB(p) -n=q.m(0,$.E()) -if(n===0)A.u(A.aY("Division by zero")) -n=r.q(0,p>0?A.O(10).J(p):$.Z()) -return A.hz(n,q.q(0,p<0?A.O(10).J(-p):$.Z()),p,o).i(0)}} -A.dK.prototype={ -aS(){return new A.ao(new A.bD(new A.bi($.E())),t._)}, -b7(a,b){var s,r=t._.a(b).a,q=A.c(a.j(0,1)),p=A.c(a.j(0,2)) -if(!(p>=0&&p<6))return A.b(B.i,p) -s=B.i[p] -p=r.b -if(p!=null)p=p!==q||r.c!==s -else p=!1 -if(p)A.u(A.J("Average scale and rounding must be constant.",null)) -r.b=q -r.c=s -if(a.j(0,0)!=null)r.a.ai(0,A.ay(a.j(0,0)),1)}, -bI(a,b){t._.a(b) -if(a.j(0,0)!=null)b.a.a.ai(0,A.ay(a.j(0,0)),-1)}, -bS(a){return t._.a(a).a.an()}, -bF(a){return t._.a(a).a.an()}, -$ie3:1} -A.dR.prototype={} -A.h4.prototype={ -$3(a,b,c){var s,r,q,p,o,n,m -A:{if("time"===b){s=A.ky(a).a -s=A.hH(s+A.jC(s,c,!1)) -break A}if("local_datetime"===b){s=A.kx(a).bT(c) -break A}if("instant"===b){r=A.fR(A.hY(a)) -q=A.hF(A.hG(A.eO(r),A.eM(r),A.eJ(r)),A.eB(A.eK(r),A.eL(r),A.eN(r),A.hJ(r)*1000+r.b)).bT(c) -p=q.a -s=q.b.a -o=B.a.l(s,36e8) -n=B.a.n(B.a.l(s,6e7),60) -m=B.a.n(B.a.l(s,1e6),60) -s=B.a.n(s,1e6) -s=A.fR(A.it(p.a,p.b,p.c,o,n,m,B.a.l(s,1000),B.a.n(s,1000))) -break A}s=A.u(A.J("Unknown temporal storage.",null))}return s}, -$S:49} -A.h_.prototype={ -$1(a){var s,r,q,p,o -if(a.j(0,0)==null)return null -s=A.A(a.j(0,1)) -r=A.c(a.j(0,2)) -q=this.a -p=a.j(0,0) -p.toString -o=q.$3(p,s,r) -if(this.b){p=a.j(0,0) -p.toString -return J.ab(o,q.$3(p,s,6))?1:0}return o instanceof A.a3?A.jq(o):J.b4(o)}, -$S:12} -A.h0.prototype={ -$1(a){return a.j(0,0)==null?null:A.jq(t.k.a(A.hY(a.j(0,0))))}, -$S:12} -A.h2.prototype={ -$1$2(a,b,c){A.mC(c,c.h("t<0>"),"T","call") -this.a.bz(new A.h3(c.h("0?(h)").a(b)),"orm_"+a+"_v1")}, -$2(a,b){return this.$1$2(a,b,t.q)}, -$S:23} -A.h3.prototype={ -$2(a,b){var s,r,q -if(a==b)return 0 -if(a==null)return-1 -if(b==null)return 1 -s=this.a -r=s.$1(a) -q=s.$1(b) -s=r!=null -if(s&&q!=null)return J.km(r,q) -if(s)return-1 -if(q!=null)return 1 -return B.c.m(a,b)}, -$S:16} -A.h1.prototype={ -$1(a){var s,r -try{s=A.hY(a) -return s}catch(r){if(A.I(r) instanceof A.v)return null -else throw r}}, -$S:24} -A.hp.prototype={ -$1(a){return A.A(a)}, -$S:25} -A.hq.prototype={ -$1(a){var s=J.ko(t.W.a(a),A.n6(),t.X) -s=A.da(s,s.$ti.h("V.E")) -return s}, -$S:21} A.ci.prototype={} A.hy.prototype={} A.a0.prototype={ @@ -5103,7 +5103,7 @@ return b instanceof A.aj&&this.a===b.a}, gu(a){return B.a.gu(this.a)}, $it:1} A.aq.prototype={ -bT(a){var s=864e8,r=this.b.a,q=this.a,p=A.iy(A.jC(r,a,q.d<2451545)).a,o=A.fS(p,s),n=r+B.a.n(p,s) +bT(a){var s=864e8,r=this.b.a,q=this.a,p=A.iy(A.jB(r,a,q.d<2451545)).a,o=A.fS(p,s),n=r+B.a.n(p,s) return A.hF(q.bs(o+B.a.l(n,s)),A.hH(B.a.n(n,s)))}, i(a){var s=this.a,r=s.gaB(),q=this.b.i(0) s=s.a<=0?" BC":"" @@ -5158,7 +5158,7 @@ q=o.a(new A.en(c,q)) o=p.a k=o.V(s,1) o=o.d -j=A.h8(o,"dart_sqlite3_create_window_function",[p.b,k,a.a,r,new A.a7(i,m,n,q,l,i)],t.S) +j=A.fX(o,"dart_sqlite3_create_window_function",[p.b,k,a.a,r,new A.a7(i,m,n,q,l,i)],t.S) o.dart_sqlite3_free(k) if(j!==0)A.cM(this,j,i,i,i)}, a7(a,b,c,d,e){var s,r @@ -5183,7 +5183,7 @@ if(p.r)return p.r=!0 s=p.b r=s.b4() -q=r!==0?A.i6(p.a,s,r,"closing database",null,null):null +q=r!==0?A.i5(p.a,s,r,"closing database",null,null):null if(q!=null)throw A.d(q)}, a9(a){var s,r,q,p=this,o=B.y if(J.ac(o)===0){if(p.r)A.u(A.at("This database has already been closed")) @@ -5191,7 +5191,7 @@ r=p.b q=r.a s=q.V(B.h.P(a),1) q=q.d -r=A.h8(q,"sqlite3_exec",[r.b,s,0,0,0],t.S) +r=A.fX(q,"sqlite3_exec",[r.b,s,0,0,0],t.S) q.dart_sqlite3_free(s) if(r!==0)A.cM(p,r,"executing",a,o)}else{s=p.al(a,!0) try{r=s @@ -5262,33 +5262,33 @@ return r}, $S(){return this.a.h("ao<0>(aD)")}} A.er.prototype={ $2(a,b){t.w.a(b) -A.hZ(a,new A.es(this.b,this.a.$1(a)),b)}, +A.hY(a,new A.es(this.b,this.a.$1(a)),b)}, $S:8} A.es.prototype={ $1(a){return this.a.b7(a,this.b)}, $S:15} A.eo.prototype={ -$1(a){A.js(a,new A.ep(a,this.a,this.b))}, +$1(a){A.jr(a,new A.ep(a,this.a,this.b))}, $S:9} A.ep.prototype={ $0(){var s=this.b.h("ao<0>?").a(this.a.gbA()),r=this.c return r.bF(s==null?r.aS():s)}, $S:17} A.en.prototype={ -$1(a){A.js(a,new A.ek(this.a,this.b,a))}, +$1(a){A.jr(a,new A.ek(this.a,this.b,a))}, $S:9} A.ek.prototype={ $0(){return this.a.bS(this.b.$1(this.c))}, $S:17} A.em.prototype={ $2(a,b){t.w.a(b) -A.hZ(a,new A.el(this.b,this.a.$1(a)),b)}, +A.hY(a,new A.el(this.b,this.a.$1(a)),b)}, $S:8} A.el.prototype={ $1(a){return this.a.bI(a,this.b)}, $S:15} A.et.prototype={ -$2(a,b){A.hZ(a,this.a,t.w.a(b))}, +$2(a,b){A.hY(a,this.a,t.w.a(b))}, $S:8} A.ej.prototype={ $0(){var s,r,q,p,o,n @@ -5338,7 +5338,7 @@ m=new A.e() j=new A.dA(r,k,m) r=r.r if(r!=null)r.bv(j,k,m) -if(n!==0){i=A.i6(s,j,n,"opening the database",null,null) +if(n!==0){i=A.i5(s,j,n,"opening the database",null,null) j.b4() throw A.d(i)}A.c(p.sqlite3_extended_result_codes(k,1)) return new A.cY(s,j,!1)}, @@ -5497,7 +5497,7 @@ if(p==null){p=new A.bf(new Uint8Array(0),0) r.A(0,q,p)}s=b+a.length if(s>p.b)p.sk(0,s) p.T(0,b,s,a)}} -A.hk.prototype={ +A.h8.prototype={ $1(a){return A.A(a).length!==0}, $S:32} A.bs.prototype={ @@ -5566,7 +5566,7 @@ r=e==null q=!r?new A.a7(e,n,n,n,n,n):new A.a7(n,f,d,n,n,n) m=m.d p=r?1:0 -o=A.h8(m,"dart_sqlite3_create_function_v2",[this.b,s,c,a,p,q],t.S) +o=A.fX(m,"dart_sqlite3_create_function_v2",[this.b,s,c,a,p,q],t.S) m.dart_sqlite3_free(s) return o}, c5(a,b,c,d){return this.c6(a,b,c,null,d,null)}, @@ -5577,7 +5577,7 @@ r.dart_sqlite3_free(s.b) r.dart_sqlite3_free(s.c) r.dart_sqlite3_free(s.d)}, b5(a,b,c){var s,r,q,p=this,o=p.a,n=o.a,m=p.c -o=A.h8(n.d,"sqlite3_prepare_v3",[o.b,p.b+a,b,c,m,p.d],t.S) +o=A.fX(n.d,"sqlite3_prepare_v3",[o.b,p.b+a,b,c,m,p.d],t.S) s=A.ak(t.a.a(n.b.buffer),0,null) m=B.a.B(m,2) if(!(m864e13)A.u(A.P(s,-864e13,864e13,"millisecondsSinceEpoch",null)) -A.h9(!1,"isUtc",t.v) +A.fY(!1,"isUtc",t.v) r=new A.a3(s,0,!1) q=this.b q===$&&A.D() @@ -5908,7 +5908,7 @@ A.fa.prototype={ $0(){var s=this.a.a,r=A.B(v.G.Object),q=A.B(r.create.apply(r,[null])) q.error_log=A.bH(s.gdf()) q.localtime=A.af(s.gdd()) -q.xOpen=A.i0(s.gdR()) +q.xOpen=A.i_(s.gdR()) q.xDelete=A.fT(s.gdF()) q.xAccess=A.bI(s.gdv()) q.xFullPathname=A.bI(s.gdN()) @@ -5929,13 +5929,13 @@ q.xFileControl=A.fT(s.gdJ()) q.xSectorSize=A.bH(s.gdX()) q["dispatch_()v"]=A.bH(s.gcR()) q["dispatch_()i"]=A.bH(s.gcN()) -q.dispatch_update=A.i0(s.gcP()) +q.dispatch_update=A.i_(s.gcP()) q.dispatch_xFunc=A.bI(s.gcX()) q.dispatch_xStep=A.bI(s.gd0()) q.dispatch_xInverse=A.bI(s.gcZ()) q.dispatch_xValue=A.af(s.gd2()) q.dispatch_xFinal=A.af(s.gcV()) -q.dispatch_compare=A.i0(s.gcT()) +q.dispatch_compare=A.i_(s.gcT()) q.dispatch_busy=A.af(s.gcL()) q.changeset_apply_filter=A.af(s.gcJ()) q.changeset_apply_conflict=A.fT(s.gcH()) @@ -6091,15 +6091,15 @@ A.bf.prototype={};(function aliases(){var s=J.aT.prototype s.c8=s.i s=A.k.prototype s.b9=s.L})();(function installTearOffs(){var s=hunkHelpers._static_1,r=hunkHelpers._static_0,q=hunkHelpers.installStaticTearOff,p=hunkHelpers._instance_1u,o=hunkHelpers._instance_2u,n=hunkHelpers.installInstanceTearOff -s(A,"mz","lg",4) -s(A,"mA","lh",4) -s(A,"mB","li",4) -r(A,"jE","mu",0) +s(A,"mx","lg",4) +s(A,"my","lh",4) +s(A,"mz","li",4) +r(A,"jD","ms",0) q(A,"n6",1,null,["$2$parameter","$1"],["jQ",function(a){return A.jQ(a,!1)}],56,0) s(A,"n5","mY",57) -s(A,"n3","kS",58) -s(A,"n4","kT",42) -s(A,"n2","kR",40) +s(A,"n1","kS",58) +s(A,"n2","kT",42) +s(A,"n0","kR",40) var m p(m=A.cX.prototype,"gdf","dg",1) o(m,"gdd","de",34) @@ -6135,7 +6135,7 @@ o(m,"gcL","cM",51) o(m,"gcJ","cK",52) n(m,"gcH",0,3,null,["$3"],["cI"],53,0,0)})();(function inheritance(){var s=hunkHelpers.mixin,r=hunkHelpers.inherit,q=hunkHelpers.inheritMany r(A.e,null) -q(A.e,[A.hC,J.d3,A.cf,J.bR,A.q,A.eP,A.f,A.b9,A.c6,A.bh,A.ch,A.T,A.aH,A.bT,A.f_,A.eF,A.bY,A.cA,A.aP,A.a_,A.ez,A.c4,A.b8,A.d7,A.fA,A.fk,A.fJ,A.al,A.dN,A.fH,A.fF,A.dF,A.ad,A.dI,A.bj,A.F,A.dG,A.dZ,A.cG,A.k,A.bS,A.cW,A.fM,A.cF,A.z,A.ct,A.a3,A.aR,A.fm,A.dk,A.cl,A.fn,A.v,A.d2,A.M,A.e_,A.cm,A.d0,A.eE,A.dQ,A.dj,A.dx,A.eV,A.cj,A.fO,A.eZ,A.dJ,A.bi,A.dL,A.bD,A.dK,A.dR,A.ci,A.hy,A.a0,A.az,A.ap,A.aj,A.aq,A.ck,A.ao,A.b5,A.cY,A.eX,A.cU,A.bs,A.H,A.cQ,A.dW,A.dS,A.b7,A.aZ,A.eW,A.dC,A.dA,A.fc,A.dD,A.b_,A.au,A.cX,A.a7,A.f9,A.fB,A.f4]) +q(A.e,[A.hC,J.d3,A.cf,J.bR,A.q,A.eP,A.f,A.b9,A.c6,A.bh,A.ch,A.T,A.aH,A.bT,A.f_,A.eF,A.bY,A.cA,A.aP,A.a_,A.ez,A.c4,A.b8,A.d7,A.fA,A.fk,A.fJ,A.al,A.dN,A.fH,A.fF,A.dF,A.ad,A.dI,A.bj,A.F,A.dG,A.dZ,A.cG,A.k,A.bS,A.cW,A.fM,A.cF,A.z,A.ct,A.a3,A.aR,A.fm,A.dk,A.cl,A.fn,A.v,A.d2,A.M,A.e_,A.cm,A.d0,A.eE,A.dQ,A.dj,A.dx,A.eV,A.cj,A.bi,A.dL,A.bD,A.dK,A.eZ,A.dJ,A.dR,A.fO,A.ci,A.hy,A.a0,A.az,A.ap,A.aj,A.aq,A.ck,A.ao,A.b5,A.cY,A.eX,A.cU,A.bs,A.H,A.cQ,A.dW,A.dS,A.b7,A.aZ,A.eW,A.dC,A.dA,A.fc,A.dD,A.b_,A.au,A.cX,A.a7,A.f9,A.fB,A.f4]) q(J.d3,[J.d5,J.c1,J.c2,J.U,J.bw,J.bv,J.aS]) q(J.c2,[J.aT,J.r,A.aU,A.c9]) q(J.aT,[J.dl,J.bg,J.aB]) @@ -6152,10 +6152,10 @@ r(A.b0,A.aH) q(A.b0,[A.cy,A.bF,A.cz]) r(A.bU,A.bT) r(A.cb,A.aE) -q(A.aP,[A.cS,A.cT,A.du,A.hf,A.hh,A.fe,A.fd,A.fP,A.fx,A.fE,A.fj,A.hl,A.hm,A.ho,A.hn,A.fU,A.fV,A.fW,A.fX,A.fZ,A.h4,A.h_,A.h0,A.h2,A.h1,A.hp,A.hq,A.eY,A.eq,A.es,A.eo,A.en,A.el,A.hk,A.eT,A.f5,A.f6,A.f7,A.f8]) +q(A.aP,[A.cS,A.cT,A.du,A.h3,A.h5,A.fe,A.fd,A.fP,A.fx,A.fE,A.fj,A.h9,A.ha,A.hb,A.hc,A.hd,A.he,A.hg,A.hm,A.hh,A.hi,A.hk,A.hj,A.hp,A.hq,A.ho,A.hn,A.eY,A.eq,A.es,A.eo,A.en,A.el,A.h8,A.eT,A.f5,A.f6,A.f7,A.f8]) q(A.du,[A.dt,A.br]) r(A.c3,A.a_) -q(A.cT,[A.hg,A.fQ,A.h7,A.fy,A.eD,A.fi,A.fY,A.h3,A.er,A.em,A.et]) +q(A.cT,[A.h4,A.fQ,A.fW,A.fy,A.eD,A.fi,A.hf,A.hl,A.er,A.em,A.et]) r(A.by,A.aU) q(A.c9,[A.c7,A.L]) q(A.L,[A.cu,A.cw]) @@ -6166,7 +6166,7 @@ r(A.a6,A.cx) q(A.c8,[A.dc,A.dd]) q(A.a6,[A.de,A.df,A.dg,A.dh,A.di,A.ca,A.bc]) r(A.bG,A.dM) -q(A.cS,[A.ff,A.fg,A.fG,A.fp,A.ft,A.fs,A.fr,A.fq,A.fw,A.fv,A.fu,A.fD,A.h6,A.fL,A.fK,A.eu,A.ep,A.ek,A.ej,A.e8,A.e5,A.ea,A.ec,A.ee,A.e7,A.ed,A.ei,A.eg,A.ef,A.e9,A.eb,A.eh,A.e6,A.fa]) +q(A.cS,[A.ff,A.fg,A.fG,A.fp,A.ft,A.fs,A.fr,A.fq,A.fw,A.fv,A.fu,A.fD,A.fV,A.fL,A.fK,A.eu,A.ep,A.ek,A.ej,A.e8,A.e5,A.ea,A.ec,A.ee,A.e7,A.ed,A.ei,A.eg,A.ef,A.e9,A.eb,A.eh,A.e6,A.fa]) r(A.cq,A.dI) r(A.dV,A.cG) r(A.d_,A.bS) @@ -6196,7 +6196,7 @@ s(A.dT,A.k) s(A.dU,A.dj) s(A.dW,A.dx) s(A.dX,A.a_)})() -var v={G:typeof self!="undefined"?self:globalThis,typeUniverse:{eC:new Map(),tR:{},eT:{},tPV:{},sEA:[]},mangledGlobalNames:{a:"int",m:"double",a2:"num",h:"String",aO:"bool",M:"Null",j:"List",e:"Object",ba:"Map",p:"JSObject"},mangledNames:{},types:["~()","~(a)","a(X,a)","h?(as)","~(~())","~(a7,a,a,a)","~(@)","a(X)","~(aD,j)","~(aD)","~(a7,a)","@()","e?(as)","M(@)","M()","~(as)","a(h?,h?)","e?()","a(H,a,a,a)","a(H,a)","a(X,a,a,U)","r(j)","~(e?,e?)","~(h,0^?(h))>","a3?(h)","h(h)","a(a,a)","h(e?)","a(a)","0&()","M(p)","ai<~>(~)","aO(h)","@(@,h)","~(U,a)","X?(H,a,a,a,a)","a(H,a,a)","M(~())","a(H?,a,a)","a(as)","aq?(h)","@(@)","aj?(h)","@(h)","a(X,a,a)","M(@,aW)","a(a())","~(~(a,h,a),a,a,a,U)","~(a,@)","e(e,h,a)","a(a7,a,a,a,a)","a(a(a),a)","a(eQ,a)","a(eQ,a,a)","p()","ai

(h)","e?(e?{parameter:aO})","e?(e?)","ap?(h)","a(X,U)","M(e,aW)"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti"),rttc:{"2;":(a,b)=>c=>c instanceof A.cy&&a.b(c.a)&&b.b(c.b),"2;file,outFlags":(a,b)=>c=>c instanceof A.bF&&a.b(c.a)&&b.b(c.b),"2;result,resultCode":(a,b)=>c=>c instanceof A.cz&&a.b(c.a)&&b.b(c.b)}} +var v={G:typeof self!="undefined"?self:globalThis,typeUniverse:{eC:new Map(),tR:{},eT:{},tPV:{},sEA:[]},mangledGlobalNames:{a:"int",m:"double",a2:"num",h:"String",aO:"bool",M:"Null",j:"List",e:"Object",ba:"Map",p:"JSObject"},mangledNames:{},types:["~()","~(a)","a(X,a)","h?(as)","~(~())","~(a7,a,a,a)","~(@)","a(X)","~(aD,j)","~(aD)","~(a7,a)","@()","a(h?,h?)","M(@)","M()","~(as)","e?(as)","e?()","a(H,a,a,a)","a(H,a)","a(X,a,a,U)","ai<~>(~)","a3?(h)","h(h)","r(j)","M(p)","a(a,a)","h(e?)","a(a)","0&()","M(~())","a(as)","aO(h)","@(@,h)","~(U,a)","X?(H,a,a,a,a)","a(H,a,a)","~(e?,e?)","a(H?,a,a)","e(e,h,a)","aq?(h)","@(@)","aj?(h)","@(h)","a(X,a,a)","M(@,aW)","a(a())","~(~(a,h,a),a,a,a,U)","~(a,@)","~(h,0^?(h))>","a(a7,a,a,a,a)","a(a(a),a)","a(eQ,a)","a(eQ,a,a)","p()","ai

(h)","e?(e?{parameter:aO})","e?(e?)","ap?(h)","a(X,U)","M(e,aW)"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti"),rttc:{"2;":(a,b)=>c=>c instanceof A.cy&&a.b(c.a)&&b.b(c.b),"2;file,outFlags":(a,b)=>c=>c instanceof A.bF&&a.b(c.a)&&b.b(c.b),"2;result,resultCode":(a,b)=>c=>c instanceof A.cz&&a.b(c.a)&&b.b(c.b)}} A.lG(v.typeUniverse,JSON.parse('{"aB":"aT","dl":"aT","bg":"aT","nk":"aU","r":{"j":["1"],"i":["1"],"p":[],"f":["1"]},"d5":{"aO":[],"o":[]},"c1":{"o":[]},"c2":{"p":[]},"aT":{"p":[]},"d4":{"cf":[]},"ey":{"r":["1"],"j":["1"],"i":["1"],"p":[],"f":["1"]},"bR":{"K":["1"]},"bv":{"m":[],"a2":[],"t":["a2"]},"c0":{"m":[],"a":[],"a2":[],"t":["a2"],"o":[]},"d6":{"m":[],"a2":[],"t":["a2"],"o":[]},"aS":{"h":[],"t":["h"],"eI":[],"o":[]},"bx":{"q":[]},"i":{"f":["1"]},"V":{"i":["1"],"f":["1"]},"cn":{"V":["1"],"i":["1"],"f":["1"],"V.E":"1","f.E":"1"},"b9":{"K":["1"]},"bb":{"f":["2"],"f.E":"2"},"bV":{"bb":["1","2"],"i":["2"],"f":["2"],"f.E":"2"},"c6":{"K":["2"]},"a5":{"V":["2"],"i":["2"],"f":["2"],"V.E":"2","f.E":"2"},"cp":{"f":["1"],"f.E":"1"},"bh":{"K":["1"]},"bd":{"f":["1"],"f.E":"1"},"bW":{"bd":["1"],"i":["1"],"f":["1"],"f.E":"1"},"ch":{"K":["1"]},"ce":{"V":["1"],"i":["1"],"f":["1"],"V.E":"1","f.E":"1"},"cy":{"b0":[],"aH":[]},"bF":{"b0":[],"aH":[]},"cz":{"b0":[],"aH":[]},"bT":{"ba":["1","2"]},"bU":{"bT":["1","2"],"ba":["1","2"]},"cb":{"aE":[],"q":[]},"d8":{"q":[]},"dw":{"q":[]},"cA":{"aW":[]},"aP":{"b6":[]},"cS":{"b6":[]},"cT":{"b6":[]},"du":{"b6":[]},"dt":{"b6":[]},"br":{"b6":[]},"dr":{"q":[]},"c3":{"a_":["1","2"],"ba":["1","2"],"a_.K":"1","a_.V":"2"},"c5":{"i":["1"],"f":["1"],"f.E":"1"},"c4":{"K":["1"]},"eA":{"i":["1"],"f":["1"],"f.E":"1"},"b8":{"K":["1"]},"b0":{"aH":[]},"d7":{"l4":[],"eI":[]},"by":{"aU":[],"p":[],"o":[]},"aU":{"p":[],"o":[]},"c9":{"p":[]},"c7":{"ir":[],"p":[],"o":[]},"L":{"a4":["1"],"p":[]},"c8":{"k":["m"],"L":["m"],"j":["m"],"a4":["m"],"i":["m"],"p":[],"f":["m"],"T":["m"]},"a6":{"k":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"]},"dc":{"k":["m"],"y":["m"],"L":["m"],"j":["m"],"a4":["m"],"i":["m"],"p":[],"f":["m"],"T":["m"],"o":[],"k.E":"m"},"dd":{"k":["m"],"y":["m"],"L":["m"],"j":["m"],"a4":["m"],"i":["m"],"p":[],"f":["m"],"T":["m"],"o":[],"k.E":"m"},"de":{"a6":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"df":{"a6":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"dg":{"a6":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"dh":{"a6":[],"hM":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"di":{"a6":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"ca":{"a6":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"bc":{"a6":[],"f1":[],"k":["a"],"y":["a"],"L":["a"],"j":["a"],"a4":["a"],"i":["a"],"p":[],"f":["a"],"T":["a"],"o":[],"k.E":"a"},"dM":{"q":[]},"bG":{"aE":[],"q":[]},"ad":{"q":[]},"cq":{"dI":["1"]},"F":{"ai":["1"]},"cG":{"j0":[]},"dV":{"cG":[],"j0":[]},"k":{"j":["1"],"i":["1"],"f":["1"]},"a_":{"ba":["1","2"]},"d_":{"bS":["h","j"]},"dy":{"bS":["h","j"]},"e4":{"t":["e4"]},"a3":{"t":["a3"]},"m":{"a2":[],"t":["a2"]},"aR":{"t":["aR"]},"a":{"a2":[],"t":["a2"]},"j":{"i":["1"],"f":["1"]},"a2":{"t":["a2"]},"h":{"t":["h"],"eI":[]},"z":{"e4":[],"t":["e4"]},"ct":{"kE":["1"]},"cO":{"q":[]},"aE":{"q":[]},"ah":{"q":[]},"aC":{"q":[]},"bZ":{"aC":[],"q":[]},"co":{"q":[]},"dv":{"q":[]},"be":{"q":[]},"cV":{"q":[]},"dk":{"q":[]},"cl":{"q":[]},"d2":{"q":[]},"e_":{"aW":[]},"dQ":{"l_":[]},"dL":{"e3":["bi"]},"dK":{"e3":["bD"]},"az":{"t":["az"]},"ap":{"t":["ap"]},"aj":{"t":["aj"]},"aq":{"t":["aq"]},"as":{"j":["e?"],"i":["e?"],"f":["e?"]},"cY":{"kz":[]},"dz":{"k":["e?"],"as":[],"j":["e?"],"i":["e?"],"f":["e?"],"k.E":"e?"},"bB":{"cU":[]},"dE":{"c_":[],"bs":[],"K":["N"]},"d1":{"H":[]},"dO":{"X":[]},"N":{"dx":["h","@"],"a_":["h","@"],"ba":["h","@"],"a_.K":"h","a_.V":"@"},"c_":{"bs":[],"K":["N"]},"dq":{"k":["N"],"dj":["N"],"j":["N"],"i":["N"],"bs":[],"f":["N"],"k.E":"N"},"dS":{"K":["N"]},"b7":{"l9":[]},"cR":{"H":[]},"cQ":{"X":[]},"au":{"cc":[]},"dC":{"l0":[]},"dA":{"l1":[]},"dD":{"l2":[]},"b_":{"aD":[]},"bC":{"k":["au"],"j":["au"],"i":["au"],"f":["au"],"k.E":"au"},"cg":{"H":[]},"dY":{"X":[]},"bf":{"aG":["a"],"k":["a"],"j":["a"],"i":["a"],"f":["a"],"k.E":"a","aG.E":"a"},"aG":{"k":["1"],"j":["1"],"i":["1"],"f":["1"]},"dP":{"aG":["a"],"k":["a"],"j":["a"],"i":["a"],"f":["a"]},"kK":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"f1":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"ld":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"kI":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"hM":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"kJ":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"lc":{"y":["a"],"j":["a"],"i":["a"],"f":["a"]},"kF":{"y":["m"],"j":["m"],"i":["m"],"f":["m"]},"kG":{"y":["m"],"j":["m"],"i":["m"],"f":["m"]}}')) A.lF(v.typeUniverse,JSON.parse('{"L":1,"cW":2}')) var u={c:"Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type"} @@ -6418,16 +6418,16 @@ $.iR=null $.ip=null $.io=null $.jH=null -$.jD=null +$.jC=null $.jL=null -$.hb=null -$.hi=null +$.h_=null +$.h6=null $.i8=null $.fC=A.x([],A.b2("r?>")) $.bK=null $.cJ=null $.cK=null -$.i2=!1 +$.i1=!1 $.w=B.d $.j3=null $.j4=null @@ -6437,8 +6437,8 @@ $.hP=A.fl("_lastQuoRemDigits") $.hQ=A.fl("_lastQuoRemUsed") $.cs=A.fl("_lastRemUsed") $.hR=A.fl("_lastRem_nsh")})();(function lazyInitializers(){var s=hunkHelpers.lazyFinal,r=hunkHelpers.lazy -s($,"nd","jX",()=>A.hc("_$dart_dartClosure")) -s($,"nc","bQ",()=>A.hc("_$dart_dartClosure_dartJSInterop")) +s($,"nd","jX",()=>A.h0("_$dart_dartClosure")) +s($,"nc","bQ",()=>A.h0("_$dart_dartClosure_dartJSInterop")) s($,"nN","kj",()=>A.x([new J.d4()],A.b2("r"))) s($,"no","k2",()=>A.aF(A.f0({ toString:function(){return"$receiver$"}}))) @@ -6472,10 +6472,10 @@ q.ca() return q}) s($,"nf","ib",()=>A.kB($.E(),0)) s($,"ne","jY",()=>A.cd("^([+-]?)([0-9]*)(?:\\.([0-9]*))?(?:[eE]([+-]?[0-9]+))?$",!0)) -s($,"nM","ki",()=>A.cd("^(-?\\d{4,7})-(\\d{2})-(\\d{2})[ T](\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,6})?)?)(Z|[+-]\\d{2}(?::?\\d{2})?(?::?\\d{2})?)?( BC)?$",!1)) s($,"ni","k_",()=>A.cd("^(-?\\d{4,7})-(\\d{2})-(\\d{2})( BC)?$",!1)) s($,"nj","k0",()=>A.cd("^(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,6}))?)?$",!0)) s($,"nh","jZ",()=>A.cd("^(-?\\d{4,7}-\\d{2}-\\d{2})[ T](\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,6})?)?)( BC)?$",!1)) +s($,"nM","ki",()=>A.cd("^(-?\\d{4,7})-(\\d{2})-(\\d{2})[ T](\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,6})?)?)(Z|[+-]\\d{2}(?::?\\d{2})?(?::?\\d{2})?)?( BC)?$",!1)) s($,"na","jW",()=>$.Z().O(0,63).D(0)) s($,"n9","jV",()=>{var q=$.Z() return q.O(0,63).ac(0,q)}) @@ -6516,5 +6516,5 @@ convertToFastObject($);(function(a){if(typeof document==="undefined"){a(null) return}if(typeof document.currentScript!="undefined"){a(document.currentScript) return}var s=document.scripts function onLoad(b){for(var q=0;q.dart` unless another location -is shown. The browser report is -[`research/validation/browser.json`](../research/validation/browser.json), with -commands and limits in [SQLite web](sqlite-web.md). - -The subsequent [selection cost capture](../research/benchmarks/runtime-selection.json) -uses runtime `d0a8c9a`. Exact SQL, parameters and result/transport volumes match the -baseline. It records fewer selected List allocations, without establishing a -latency or throughput gain; the [comparison](performance.md#direct-selection-decoding) -also documents the PostgreSQL protocol lifecycle tradeoff. `selection_test` adds -direct coverage for deferred/ordered mapping, repeated fields, all typed arities, -streaming, failures and dynamic field snapshots on both native backends. - -## Final lifecycle audit - -| Design §3 scenario / acceptance gate | Verification | +# Validation guide + +Use the repository checks to verify the API and database behavior after a change. +A test name below identifies a scenario to exercise, not a claim that the current +checkout has passed it. [Progress](https://github.com/medz/dart-orm/blob/main/doc/progress.md) records completed validation; +[capabilities](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md) states database and platform limits. + +## Start with the public workflow + +From a disposable consumer project, initialize a database, generate its client, +create and review the first migration, then apply and verify it. Run a typed +projection and a transaction through generated table getters. Change a model, +regenerate, and ensure the analyzer catches stale result or input types. The +[CLI guide](https://github.com/medz/dart-orm/blob/main/doc/cli.md) and [quickstart](https://github.com/medz/dart-orm/blob/main/README.md#get-started) provide the commands. + +Verify independent imports as well: values and schema metadata need no connection; +typed SQL can compile offline; a raw driver and runtime work without generated +models; saved migrations do not import current application models. The +`layer_boundaries_test`, `runtime_test` and `sql_builder_test` suites exercise +these contracts. + +## Choose regression suites by behavior + +All names below refer to files under `test/` with the `_test.dart` suffix. + +| Changed behavior | Representative suites | | --- | --- | -| Add a defaulted field without rewriting existing create calls | Generated type/default tests and actual Android v1 → v2 migration preserve rows and populate the new field | -| Select emails only, returning `List` | Native projection/plan tests and executable cookbook scalar selection | -| Root pagination plus each parent's latest three titles | Native relation checks, `three_posts` runtime workload and browser generated-relation scenario; actual SQL counts and parameter chunks | -| Create a parent and children in one transaction | README and teams examples, native generated tests and actual Android relation write | -| Clear a nullable field and increment atomically | Generated patch/expression-write tests, NULL/default negative type checks and transaction suites | -| Separate Dart rename from physical column rename | Generated snapshot/authoring tests, CLI migration diff and real migration/recovery tests; Record field rename remains manual | -| Switch SQLite memory/file/worker storage | Native persistence and read-only tests, Chrome JS/WASM OPFS reload/upgrade, Android APK upgrade/restart | -| Keep database-specific configuration and ownership explicit | Separate typed driver inputs, wrong-backend compilation failures, borrowed PostgreSQL pools and cancellation/acquisition tests; unimplemented transports are not certified | -| Process large results and release sessions | Real SQLite/PostgreSQL cursors, batching, early stream cancellation and post-failure connection reuse; browser stream/release scenario | -| Check version, migration state and live structure before deployment | CLI/import/baseline tests, application-version tests, durable backfill and nontransactional recovery, Android bundled migration checks | -| Review query, type and unmanaged catalog coverage | [Capability review](capabilities.md); final native suite includes real triggers and complete policy/RLS metadata checks | -| Run documented onboarding and complex query usage | [Usage capture](../research/validation/usage.json): dependencies, generation, both existing examples, nine cookbook checks on each backend, migration-chain validation and clean analysis | - -The [single-engine migration correction](../research/validation/migration-targets.md) -records a full run with 845 passes and two incorrectly configured crash-test -registries, then 21 passing target/recovery tests after correcting them. It also -verifies native AOT bundles, real Chrome JS/WASM and the public CLI workflow. -Each history now fixes one engine, selected expressions alone affect fingerprints, -and wrong connections fail before migration SQL. Android runtime was -not rerun for that correction; its earlier capture remains tied to its original revision. - -The [Dart migration acceptance](../research/validation/dart-migrations.json) records -the replacement of JSON artifacts with Dart snapshots, fixed history and static -registration. Its full native run passed 837 tests and found one old fixture path; -the correction and final generator changes pass a 76-test follow-up. The record -retains both logs instead of claiming a single all-green full invocation. AOT -checks execute compiled history without consumer source files and resume historical -backfills. The original [835-test capture](../research/validation/native.json) -remains a historical baseline. - -The earlier Chrome JS/WASM and Android APK captures use the original Dart workflow source. -Android proves the native Flutter lane on its recorded emulator, including compiled -migration definitions, upgrade and process restart; it does not certify Apple -platforms or every server type/scenario. See [Flutter scope](flutter.md) and -[browser scope](sqlite-web.md) for the exact assertions. - -## Extension boundaries in the design - -Design §12 explicitly places PostgreSQL-specific JSON/array/COPY/locking and -SQLite FTS expansion behind real demand. HTTP/serverless transports require -their own session/capability verification. These are not silently provided by a -native driver or raw-SQL escape hatch. Distributed transactions, synchronization, -arbitrary object-graph tracking and a general optimizer remain separately scoped -in the design. Calendar SQL arithmetic, timezone-rule conversion and additional -native types also remain explicit extensions, as recorded in the capability -review. They are not presented as working APIs or hidden behind silent fallbacks. +| Model declarations and generated types | `generator`, `generator_dialects`, `nominal_generation`, `types`, `generated_database`, `nominal_database` | +| Build/watch and project commands | `builder`, `cli`, `cli_workflow`, `named_sql_generation`, `import_cli`, `migration_plan_cli` | +| Projections, mapping and query scope | `database`, `selection`, `query_boundary`, `union`, `plan` | +| Keys, relationships and batched loading | `relation`, `many_to_many`, `unconstrained_relation` | +| Values and custom codecs | `integer`, `custom_codec`, `temporal`, `temporal_precision`, `decimal`, `decimal_division`, `decimal_average` | +| Defaults, generated values and constraints | `client_default`, `computed`, `check` | +| Sessions, transactions and failure recovery | `transaction`, `retry`, `acquisition`, `stream`, `session_connection`, `runtime_lifecycle_review` | +| Subscriptions and execution observations | `watch`, `observation` | +| Migration history, catalog and recovery | `migration`, `migration_target`, `migration_recovery`, `backfill`, `schema_version`, `import` | +| MySQL and MariaDB behavior | `mysql_driver`, `mysql_database`, `mysql_import`, `mysql_transaction_boundary`, `mysql_migration` | + +Use real database URLs to enable the server suites. Tests create their own tables +or schemas; MySQL/MariaDB recovery tests require permission to create isolated +databases. See [contributing](https://github.com/medz/dart-orm/blob/main/doc/contributing.md) for environment variables and the +complete native command. Capability-dependent skips must be reported separately +from passes, especially when the SQLite build cannot interrupt running SQL. + +## Verify browser and Flutter behavior + +From the repository root: + +```sh +dart run tool/test_browser.dart +dart run tool/test_browser.dart --wasm +dart run tool/test_flutter_web.dart /absolute/path/to/flutter +``` + +Browser checks exercise packaged resource identity, generated queries, memory and +OPFS storage, reopen/upgrade, constraints, transactions, cursors, subscriptions, +value transport and explicit rejection of unsupported interruption. They use real +Chrome and record JS/WASM mode. The Flutter Web runner also verifies release +packaging, nested routes and configurations with and without isolation headers. + +For Android, follow the [APK upgrade and restart workflow](https://github.com/medz/dart-orm/blob/main/doc/flutter.md). Browser, +Android and native server validation are separate: one passing target does not +establish another. Keep SDK, database, browser/device, source revision and report +paths with the validation result. Reports are local outputs under `.dart_tool/`. + +## Check documentation and packaging + +After static analysis, run `dart doc --validate-links` to generate the actual API +site when changing categories, exports or documentation links. Inspect library +navigation, public type signatures and links in the generated pages. Documentation +must expose usable public APIs without presenting internal helpers as supported +entrypoints. + +Run `dart pub publish --dry-run` to inspect the package archive. Handwritten +`doc/`, examples and runtime resources belong in the package; generated `doc/api/`, +local validation output and repository-only tools do not. Verify the published +version separately when making an actual release. + +## Measure performance separately + +Use [runtime benchmarks](https://github.com/medz/dart-orm/blob/main/doc/performance.md), [generation/editor measurements](https://github.com/medz/dart-orm/blob/main/doc/generation.md) +and the [decimal cost probe](https://github.com/medz/dart-orm/blob/main/doc/decimals.md#verify-precision-and-measure-cost) for +performance work. Preserve result semantics, transaction boundaries and exact +query volume when comparing revisions. Correctness tests, allocation traces and +latency measurements answer different questions and should have separate results. diff --git a/doc/api.md b/doc/api.md index eb61af1d..9d3cead4 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,15 +1,35 @@ -# API and mental model +# Choose an entrypoint Start with an immutable Dart class, generate its client, choose a database entrypoint, and use the generated table getters. Each query belongs to that database view. Migrations have a separate, fixed history for the chosen engine. +For an application, import its generated client and one database entrypoint: + +```dart +import 'package:my_app/schema.orm.dart'; +import 'package:orm/sqlite.dart'; + +final db = await sqlite(const SqliteOptions.file('app.sqlite')); +try { + final tasks = await db.tasks.where((t) => t.done.eq(false)).get(); + print(tasks); +} finally { + await db.close(); +} +``` + +The database owns its resources until `close()` completes. PostgreSQL's +`postgres(options)` creates a lazy pool synchronously; it first connects when +an operation acquires a connection. The other connection helpers return futures +that complete after opening and validating their connection. + ## Imports SQLite uses `sqlite.dart` on native platforms and the web. `SqliteOptions.memory()` is portable. `persistent(name, nativePath: ...)` uses the native path or the named browser database. Flutter bundles its browser resources automatically; see -[SQLite Web setup](sqlite-web.md). Platform transports are internal. +[SQLite Web setup](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md). Platform transports are internal. | Module or use | Import | Purpose | | --- | --- | --- | @@ -39,6 +59,8 @@ query builder. `migrate.dart` uses raw sessions and physical metadata without model declarations or generated query code. None of these runtime/schema imports pull in the analyzer, build system or a concrete database adapter. `sqlite.dart` selects its platform implementation; each server engine has its own options. +MySQL and MariaDB entrypoints each expose their own driver and connection options. +Import public library paths; files under `lib/src/` are implementation details. The single package still declares tooling dependencies for its CLI and builders; `pub get` resolves them. This import boundary avoids runtime initialization and compilation dependencies, not the dependency-download cost of one package. @@ -92,15 +114,27 @@ named Record or DTO. A Dart mapper does not become a SQL expression. Runtime `alias.optional(selection)` checks that alias's presence. It does not prove another left-joined alias exists. Give each optional alias its own guard, or use nullable expressions. Relationships follow the same rule: `one()` can be absent, `required()` -checks presence, and `many()` loads a collection. [Relationship execution](relations.md) +checks presence, and `many()` loads a collection. [Relationship execution](https://github.com/medz/dart-orm/blob/main/doc/relations.md) documents joins, batched queries and consistency boundaries. ## Preparing and executing `where`, `select`, `orderBy`, `take`, `skip` and `map` return new descriptions. Repeated `where` adds predicates; `orderBy`, `take` and `skip` replace their own -settings. `compile()`/`inspect()` perform no I/O. `get()`/`first()`/`single()` execute -reads; a stream acquires a cursor when listened to. +settings. `compile()`/`inspect()` perform no I/O. A stream acquires a cursor when +listened to. Choose the read operation by the expected result: + +| Operation | Empty result | One row | Multiple rows | +| --- | --- | --- | --- | +| `get()` | Empty list | List of one | List of all | +| `first()` | Cardinality error | The row | First row | +| `firstOrNull()` | null | The row | First row | +| `single()` | Cardinality error | The row | Cardinality error | +| `singleOrNull()` | null | The row | Cardinality error | + +A selected SQL NULL still counts as a row. Strict reads return that null when +their result type allows it. Use a Record projection if an optional scalar read +must distinguish an absent row from a row containing null. Generated `create(...)` and `patch(...)` execute immediately. `insert(...)`, `insertMany(...)`, `update(...)` and `delete()` prepare mutations; call `execute()` @@ -111,7 +145,7 @@ constructed. Re-executing the prepared insert reuses those values. field, and `.defaultValue()` requests the database default. An empty update is an error. `RETURNING` returns scalar SQL projections; query relationships afterward. For statement deadlines or cancellation, use the prepared mutation's execution -options. [Defaults](defaults.md) and [execution](execution.md) specify the limits. +options. [Defaults](https://github.com/medz/dart-orm/blob/main/doc/defaults.md) and [execution](https://github.com/medz/dart-orm/blob/main/doc/execution.md) specify the limits. ## Connection and transaction ownership diff --git a/doc/authoring.md b/doc/authoring.md index 9b9810b0..79fc9a62 100644 --- a/doc/authoring.md +++ b/doc/authoring.md @@ -36,7 +36,7 @@ can use the same row class, and a self-join still has separate table occurrences Row identity does not determine SQL scope. Generate with `dart run orm generate lib/schema.dart`, then import the generated -client and the chosen driver. [Generation](generation.md) describes standalone +client and the chosen driver. [Generation](https://github.com/medz/dart-orm/blob/main/doc/generation.md) describes standalone and build_runner workflows. New projects can start with `dart run orm init --database sqlite`; after initialization, `dart run orm generate` reads the typed project configuration. @@ -158,16 +158,3 @@ Class fields support Dart's normal symbol navigation and rename tools. Regenerate after edits and repair affected generated-API references; a model rename does not promise that an IDE will edit the regenerated client or all its consumers automatically. - -## Earlier authoring experiment - -`dart run tool/compare_authoring.dart` retains the controlled comparison of Record, -primary-constructor and typed-table-field declarations. Its adapter deliberately -normalizes all three forms to Records so their output can be compared. It is an -experiment, not the production class reader. - -The historical [report](../research/benchmarks/authoring.json) records sources, -LSP edits, byte-identical physical snapshots and stale-consumer errors on Dart -3.13.3. Its measurements establish neither universal brevity nor a generation -speed ranking. The production generator now reads class parameters directly and -constructs the declared model type. diff --git a/doc/backfills.md b/doc/backfills.md index df3a9a50..73dfe4d5 100644 --- a/doc/backfills.md +++ b/doc/backfills.md @@ -6,7 +6,7 @@ the migration as applied. Import `package:orm/migrate.dart` alongside your datab entry point. The examples use a raw `SqlDatabase` named `db`; for an ORM `Database`, pass `db.sql` to `Migrator`. MySQL/MariaDB recovery details are documented in -[their migration guide](mysql-migrations.md). +[their migration guide](https://github.com/medz/dart-orm/blob/main/doc/mysql-migrations.md). ```dart // Use the snapshot committed with the expand migration, not current app models. @@ -59,7 +59,7 @@ dart run bin/migrate.dart apply --max-backfill-batches 10 dart run bin/migrate.dart status ``` -Configure PostgreSQL in the Dart entrypoint as described in [migrations](migrations.md). +Configure PostgreSQL in the Dart entrypoint as described in [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md). Plans report `atomic: false` when pending work includes a backfill. Bounded CLI runs additionally return `complete: false` until the entire migration list is applied. Status includes `backfill.rows`, `batches`, `last` and `upper`. Keys are diff --git a/doc/capabilities.md b/doc/capabilities.md index 8b81d1f9..a60b1a02 100644 --- a/doc/capabilities.md +++ b/doc/capabilities.md @@ -1,114 +1,101 @@ -# Capability review +# Capabilities and limits -The acceptance scope comes from the [original design](../research/new-dart-orm-design.md). -Its common relational lifecycle is implemented in one package. Optional backend -extensions and untested deployment targets are not implied by a generic driver -or a successful test on another platform. +Choose an engine and platform explicitly. The shared query API does not make +backend behavior identical: connections expose their actual capabilities and +reject unsupported operations before execution where possible. Refer to +[progress](https://github.com/medz/dart-orm/blob/main/doc/progress.md) for the revision and platforms most recently verified. ## Values and physical storage -| Design §5 requirement | Implementation and evidence | Explicit boundary | +| Value | Supported representation | Boundary | | --- | --- | --- | -| Integer widths and web-safe exact values | `IntegerBits`, integer/BigInt codecs, integer/native/browser checks, catalog metadata and reviewed migrations | Native `int` is signed 64-bit; JS-safe `int` is narrower. BigInt uses exact digits, but SQLite BigInt text does not promise numeric ordering/arithmetic. | -| Exact decimal | Exact storage, comparisons, keys and generated/imported metadata on all four engines; arithmetic, aggregates/windows and rounding on SQLite/PostgreSQL | MySQL/MariaDB reject operations that can silently lose precision, including arithmetic, SUM, explicit narrowing and decimal set operations. See [engine limits](mysql.md) and [reproductions](../research/mysql-precision-boundaries.md). | -| Distinct instant/date/time/local timestamp | UTC DateTime, LocalDate/LocalTime/LocalDateTime, explicit temporal precision, native binary decoding, SQLite collations, catalog/import/migration tests and browser transport | An IANA zone name is a separate application value, not an instant or local clock value. Calendar SQL arithmetic and timezone-rule conversion have no typed API. | -| JSON versus arbitrary objects | SqlJson distinguishes JSON null from SQL NULL; explicit domain codecs validate custom objects, including generated create/result/relationship types | A Dart Map is not an automatic entity mapping. Backend-specific JSON path APIs are extensions. | -| Stable enum text | EnumValue labels and checked enum codecs; generated/native/negative tests | Native PostgreSQL enums require separate metadata and migration support. | -| Binary and custom IDs | Uint8List and public const UseCodec declarations, checked domain codecs, generated type failures, native/browser reads/writes/relations | A codec's semantic change is not automatically a DDL change. Custom equality/order must match the declared storage. | - -The authoritative contracts and tests are indexed in [types](types.md), -[decimals](decimals.md) and the [acceptance map](acceptance.md). The design's time -contract specifies storage distinctions and precise codecs; it does not define a -timezone database or a calendar SQL function family. Earlier progress notes -listed those possible extensions as open-ended follow-up work. They remain -explicitly unimplemented rather than being counted as delivered functionality. - -Catalog import recognizes supported physical representations without sampling -data to guess semantics. Ordinary SQLite INTEGER/TEXT cannot prove a boolean, -enum, timestamp, BigInt or JSON domain. Unsupported VARCHAR/arrays/domains and -unsupported identity/table forms produce explicit issues instead of silently -changing their meaning. Reviewed annotations/codecs and migrations remain part -of adopting an existing database; import is not a universal database backup. - -## Query and execution contract - -| Required family | Evidence and available behavior | Boundary | +| Integers | Explicit integer widths and exact BigInt codecs | Native `int` is signed 64-bit; JS-safe `int` is narrower. SQLite stores BigInt as text, which does not provide numeric ordering or arithmetic. | +| Decimal | Exact storage, comparison, keys and schema metadata on all four engines | SQLite/PostgreSQL support exact arithmetic, aggregates, windows and rounding. MySQL/MariaDB reject operations that can silently lose precision; see [engine limits](https://github.com/medz/dart-orm/blob/main/doc/mysql.md). | +| Time | UTC `DateTime`, `LocalDate`, `LocalTime`, `LocalDateTime` and explicit temporal precision | An IANA zone name is a separate application value. Calendar SQL arithmetic and timezone-rule conversion have no typed API. | +| JSON | `SqlJson` distinguishes JSON null from SQL NULL | A Dart Map is not an entity mapping. Backend-specific JSON path operations require explicit SQL. | +| Enum | Checked enum codecs and stable `EnumValue` text labels | Native PostgreSQL enums need separate metadata and migration support. | +| Binary and custom IDs | `Uint8List` and public const `UseCodec` declarations | Codec changes do not automatically produce DDL. Domain equality and ordering must match the chosen storage. | + +See [types and codecs](https://github.com/medz/dart-orm/blob/main/doc/types.md) and [decimals](https://github.com/medz/dart-orm/blob/main/doc/decimals.md) for exact conversion, +precision and nullability rules. Catalog import reads physical metadata without +sampling rows to guess domain semantics. Ordinary SQLite INTEGER/TEXT cannot +prove a boolean, enum, timestamp, BigInt or JSON domain. Unsupported column and +table forms produce review issues. Import does not recreate an entire database. + +## Queries and execution + +| Operation | Behavior | Boundary | | --- | --- | --- | -| Scalars, Records, DTOs and dynamic selection | `selection_test`, generated/native tests; mappers run only after rows arrive | Dynamic field sets return a dynamic map, not invented static fields. | -| Conditions, ordering and pagination | Database/type/cursor suites, stable unique tie breakers, explicit NULL order | Offset pages do not provide a snapshot; cursor order needs declared keys. | -| Joins, grouping/HAVING, subqueries, CTEs, windows, UNION | `test/support/advanced.dart`, set and numeric suites on both backends; SQL scope and aggregate validation | INTERSECT/EXCEPT and arbitrary database functions do not have dedicated typed methods. Parameterized raw/named SQL is the formal extension path. | -| Relations and relation ordering | Joined or batched to-one; batched collections, composite/self/multiple edges, many-to-many payloads, per-parent limits, any/none/every/count | Order root rows through a typed alias or correlated count. Loaded Record fields are not SQL expressions. No cross-database relation or invisible lazy I/O. | -| Writes and transactions | Generated create/patch, explicit NULL/default, computed/client defaults, batch/upsert/RETURNING, savepoints, rollback, escaped-session rejection | No graph tracking, implicit flush or distributed transaction. | -| Failure handling and streaming | Real interruption, unknown commit, bounded acquisition/retries, real cursors and awaited cleanup | An external side effect is not rolled back by SQL or safe to repeat automatically. Web interruption and MySQL/MariaDB streaming/cancellation are unsupported and rejected. | -| Plans and observations | SQL/column/key/join/batch descriptions and actual acquisition/query/decode events | No general optimizer, result cache or hidden EXPLAIN ANALYZE. Timing scopes are documented, not total CPU attribution. | - -See [query examples](queries.md), [relationships](relations.md), -[execution](execution.md) and [observability](observability.md). - -SQLite cancellation is a build capability, not a promise for every native target. -The default Linux asset in `sqlite3` 3.6.0 hides `sqlite3_interrupt`; it rejects -statement cancellation, execution deadlines and bounded retries before SQL starts. -Ordinary transactions and streaming still work. macOS and Android interruption -have separate runtime evidence; see [execution](execution.md). - -## Migration and database adoption - -Each history fixes one database engine, even when empty. A saved migration contains -one step list and only that engine's frozen physical expressions. Mixed histories -and mismatched connections fail before migration SQL. PostgreSQL migration -execution requires 18+, and SQLite opening requires 3.35+; arbitrary reviewed SQL -can require additional capabilities. - -MySQL 8.4+ and MariaDB 10.6+ use separate driver identities. Migration execution -requires MySQL 8.4+ or MariaDB 11.8+. Their DDL can commit implicitly: checked -before/after table metadata and durable checkpoints support recovery, while -backfill writes and completion checkpoints -share a transaction. See [MySQL/MariaDB migrations](mysql-migrations.md). Current -live acceptance uses MySQL 8.4 and MariaDB 11.8; the MariaDB driver's lower -connection minimum has not received that full acceptance suite. - -Saved migrations contain immutable snapshots, operations, predecessor checksums -and explicit renames/conversions. Tests exercise fresh replay, older-version -upgrades, safe SQLite rebuilds, PostgreSQL constraint/catalog behavior, baseline, -drift, version compatibility, durable backfills and nontransactional recovery. -The migration CLI and real build_runner process workflows test application-facing -commands, including failure repair and stale-output cleanup. - -Unmanaged views, triggers, specialized indexes, policies and table options require -review. PostgreSQL policy reports now retain roles, command, mode and expressions, -plus enabled/forced row-security flags. Actual triggers are created and exercised -after read-only import/verification on both backends. Baseline verifies declared -schema facts and returns unmanaged metadata; it does not recreate or certify -grants, every extension, authorization behavior or the whole database environment. - -## Platform and tooling scope - -Native SQLite and PostgreSQL are checked against SQLite 3.53.4/PostgreSQL 18.4. -MySQL 8.4 and MariaDB 11.8 have separate live driver, typed query, import, -transaction and migration-recovery suites. -Browser reports cover real Chrome JS and Dart WASM, worker memory/OPFS storage, -reopen, interruption recovery, upgrade and watch. Android Flutter checks cover -actual debug-to-AOT APK replacement and independent-process restart. These do -not certify every database version, browser, physical device, iOS or macOS Flutter. -Current source/validation revisions are recorded in [progress](progress.md). -Each platform capture is tied to its recorded source revision; consult the latest -acceptance record before relying on a historical platform result. - -Driver configuration is typed and explicit. PostgreSQL reuses its driver's pool, -offers borrowed-pool ownership and certificate-verifying TLS by default; SQLite -owns one background connection behind a unified native/browser entrypoint. -Memory and named persistent settings are shared; native paths remain explicit. -Flutter Web bundles default assets, with optional browser resource overrides. URL options are -not silently merged with typed settings. MySQL/MariaDB own one queued physical -connection each, with certificate-verifying TLS by default and explicit capability -limits. Unsupported transports, serverless sessions, replica routing and additional -connection-pool variants need their own adapters and verification. - -Generation and completion measurements cover 10/100/1000 models. The declaration -experiment normalized three input forms to identical generated APIs. The current -package supports ordinary immutable model classes with nominal results, alongside -Record declarations; see [authoring](authoring.md). Named Record field rename is unavailable -in the checked SDK. The captured same-session class rename timeout has a checked -server-restart path; it is not described as a working general IDE workflow. -Runtime cost reports compare identical SQL/driver/result workloads, including a -controlled TCP delay, with their sampling and allocation limits stated explicitly. +| Selection | Scalar, positional/named Record, DTO and runtime field selection | Mappers execute after rows arrive. Runtime field sets return a dynamic map. | +| Pagination | Offset/limit and typed keyset cursors with unique tie breakers | Offset pages are not snapshots; nullable cursor fields need explicit NULL ordering. | +| SQL composition | Joins, grouping/HAVING, subqueries, CTEs, windows and UNION | Scope and aggregate rules are validated. INTERSECT/EXCEPT and arbitrary functions require raw or named SQL. | +| Relationships | Joined or batched to-one values, batched collections, composite/self keys and per-parent limits | No cross-database navigation or lazy property reads that issue hidden SQL. | +| Writes | Generated create/patch, omitted versus NULL/default values, expression writes, batches, upsert and RETURNING where supported | No tracked object graph or implicit flush. Computed fields are read-only. | +| Transactions | Explicit session ownership, savepoints, rollback and bounded opt-in retries | External side effects are not rolled back or made safe to repeat. Distributed transactions are not supported. | +| Observation | Query descriptions and acquisition/query/decode events | No general optimizer or result cache. Timings have explicit scopes, not total CPU attribution. | + +Choose `first()`/`single()` when absence is an error and `firstOrNull()`/ +`singleOrNull()` when it is expected. Both single-result variants reject multiple +rows. See [queries](https://github.com/medz/dart-orm/blob/main/doc/queries.md), [relationships](https://github.com/medz/dart-orm/blob/main/doc/relations.md), +[execution](https://github.com/medz/dart-orm/blob/main/doc/execution.md) and [observability](https://github.com/medz/dart-orm/blob/main/doc/observability.md). + +SQLite interruption depends on its compiled library. The default Linux asset in +`sqlite3` 3.6.0 does not expose `sqlite3_interrupt`; statement cancellation, +execution deadlines and bounded retries are rejected before SQL starts. Ordinary +transactions and cursor streaming still work. Browser SQLite has no synchronous +statement interruption either. + +MySQL/MariaDB do not support cursor streaming or cancellation tokens. Their +statement timeout discards the connection, and a submitted write can have an +unknown outcome. Do not infer rollback from a timeout or connection loss. See +[MySQL and MariaDB](https://github.com/medz/dart-orm/blob/main/doc/mysql.md) for transaction and numeric restrictions. + +## Migrations and database adoption + +Each history fixes one engine, including an empty history. A saved migration has +one step list, its reviewed fingerprint and only that engine's frozen expressions. +Mixed histories and mismatched connections fail before migration SQL. Changing a +connection URL does not translate a history. + +SQLite opening requires 3.35+ and PostgreSQL migration execution requires 18+. +MySQL connections and migrations require 8.4+; MariaDB connections require 10.6+ +and migrations require 11.8+. The MariaDB driver's lower connection minimum has +not received the full 11.8 validation matrix. Reviewed SQL can require additional +server features beyond these minimum gates. + +MySQL/MariaDB DDL can commit implicitly. Checked before/after metadata and durable +checkpoints support recovery; backfill writes and their completion checkpoints +share a transaction. See [migration recovery](https://github.com/medz/dart-orm/blob/main/doc/mysql-migrations.md). SQLite rebuilds +and PostgreSQL DDL use their respective transactional behavior. + +Renames and conversions require explicit decisions. Catalog verification checks +declared schema facts and reports unmanaged objects such as views, triggers, +specialized indexes, policies and table options. PostgreSQL reports include policy +roles, command, mode and expressions plus enabled/forced row-security flags. +Baseline does not certify grants, every extension, authorization behavior or the +entire database environment. See [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md) and [importing](https://github.com/medz/dart-orm/blob/main/doc/importing.md). + +## Connections, platforms and tools + +PostgreSQL uses its driver's pool and supports borrowed-pool ownership. SQLite +owns one background connection, with native and browser implementations behind a +single public entrypoint. MySQL/MariaDB own one queued physical connection each. +Server drivers verify TLS certificates by default; typed settings and URL options +are not silently combined. + +SQLite memory and persistent options share one API. Native applications choose +an application-owned path; browsers choose a named OPFS database. Flutter Web +bundles matching worker/WASM resources. [SQLite Web](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md) documents +resource overrides, secure-context requirements and exclusive ownership. + +The release baseline covers native SQLite/PostgreSQL, real MySQL 8.4/MariaDB 11.8, +Chrome JS/WASM, Flutter Web release builds and an Android emulator's debug-to-AOT +upgrade and process restart. This does not certify every server version, browser, +physical device, iOS or macOS Flutter. Remote/serverless transports, replica +routing and alternative pools need their own adapters and validation. + +Immutable model classes and Record declarations share the generator; classes +retain nominal result types. Editor symbol refactoring depends on the Dart SDK. +Regenerate and analyze after schema edits. [Generation](https://github.com/medz/dart-orm/blob/main/doc/generation.md) describes +build/watch behavior and editor probes; [performance](https://github.com/medz/dart-orm/blob/main/doc/performance.md) describes +reproducible cost measurements without implying universal latency guarantees. diff --git a/doc/checks.md b/doc/checks.md index 43b918f7..b99b399f 100644 --- a/doc/checks.md +++ b/doc/checks.md @@ -68,7 +68,7 @@ immutable constant expressions, so this is not a promise of zero function evaluation. No `ANALYZE` option is used. See [EXPLAIN](https://www.postgresql.org/docs/18/sql-explain.html). SQL/functions must be available in the selected database schema during verification. -[Import](importing.md) retains native check SQL and names, including explicit null +[Import](https://github.com/medz/dart-orm/blob/main/doc/importing.md) retains native check SQL and names, including explicit null names on SQLite, and emits a nonblocking `IMPORT.CHECK_SQL` review note. Check other-dialect overrides before deploying an imported declaration elsewhere. diff --git a/doc/cli.md b/doc/cli.md index 93a273de..7b1d556d 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -94,7 +94,7 @@ The first `apply` can create that file. `create --allow-destructive` permit writing reviewed drop operations, not executing them. `apply --max-backfill-batches ` bounds a resumable backfill invocation. -See [migrations](migrations.md) for immutable history, engine boundaries, reviewed +See [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md) for immutable history, engine boundaries, reviewed renames/conversions, baseline and recovery behavior. ## Explicit tools diff --git a/doc/contributing.md b/doc/contributing.md index cf2fe317..af169124 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -25,6 +25,29 @@ Server TLS defaults to `verifyFull`. Self-signed local fixtures can explicitly set `ORM_TEST_MYSQL_TLS=require` and `ORM_TEST_MARIADB_TLS=require`. Run `dart test --concurrency=1` when testing the complete native matrix. +## Documentation and public APIs + +Keep public entrypoints explicit and add `///` comments at the declarations they +export. Explain result shape, ownership, transaction boundaries and errors where +those affect callers. Internal modules use ordinary imports and exports; do not +introduce `part` directives to share private state. + +Handwritten guides live in `doc/`. Dartdoc categories connect those guides to API +navigation. After changing comments, exports or category configuration, run: + +```sh +dart doc --validate-links +``` + +After moving or renaming APIs, remove the previous generated `doc/api/` directory +before regenerating so stale pages cannot survive. Broken links, ambiguous +canonical exports and unresolved symbol references fail documentation generation. + +Inspect the generated `doc/api/` pages through a local HTTP server, including +library/category navigation, symbol links and public signatures. Do not commit +or publish that generated directory. Run package analysis and the relevant +examples as well; a formatted code block alone does not verify an example. + ## Release checks Check formatting and run `dart pub publish --dry-run`. The published package diff --git a/doc/decimals.md b/doc/decimals.md index 46074184..8dbd4a84 100644 --- a/doc/decimals.md +++ b/doc/decimals.md @@ -8,7 +8,7 @@ retained; `toString()` produces ordinary canonical decimal text. The SQL arithmetic examples below apply to SQLite and PostgreSQL. MySQL/MariaDB support exact values, storage, comparisons and MIN/MAX within 65-digit/30-scale limits, require declared column precision, and reject typed operations that can -silently lose precision. See [engine-specific boundaries](mysql.md). Pure Dart +silently lose precision. See [engine-specific boundaries](https://github.com/medz/dart-orm/blob/main/doc/mysql.md). Pure Dart `Decimal` value arithmetic is independent of the selected database. ```dart @@ -253,56 +253,32 @@ text keys `2` and `2.00` become duplicate numeric keys, so the migration fails a rolls back data, structure and history together. Repair the data before retrying. Historical backfills retain exact decimal keys in their resumable checkpoints. -See the [generated fixture](../test/support/decimals/schema.dart), -[database checks](../test/decimal_test.dart), -[division/window checks](../test/decimal_division_test.dart), -[average checks](../test/decimal_average_test.dart) and the -[native acceptance executable](../test/support/decimals/native.dart). -These establish correctness on the tested native databases. The real -[JS/WASM capture](sqlite-web.md) additionally checks exact Decimal transport. -It does not rerun this complete arithmetic/precision suite, and the Android -Flutter acceptance does not include Decimal-specific scenarios. - -## Native cost probe - -The [benchmark script](../tool/benchmark_decimal.dart) fetches and decodes 10000 -integer-valued Decimal rows. Each case has one warmup and three measured runs. -The checked-in [raw report](../research/benchmarks/decimal-division.json) was -recorded with a native macOS ARM64 AOT executable, Dart 3.13.3, in-memory SQLite -3.51.0 and local PostgreSQL 18.4. Median elapsed milliseconds: - -| Operation | SQLite | PostgreSQL | -| --- | ---: | ---: | -| Read Decimal values | 10.13 | 22.72 | -| Divide by 3, scale 2, half-even | 30.09 | 32.63 | -| Round to tens, half-even | 21.64 | 25.25 | -| Running sum then divide by 3 | 48.17 | 36.85 | - -The expanded [average report](../research/benchmarks/decimal-average.json) uses -the same native AOT method and database versions. All cases consume 10000 rows; -the ordinary average returns one row, while the running average returns 10000: - -| Operation | Returned rows | SQLite ms | PostgreSQL ms | -| --- | ---: | ---: | ---: | -| Average, scale 2, half-even | 1 | 8.78 | 2.74 | -| Running average, scale 2, half-even | 10000 | 29.12 | 47.25 | - -Both reports retain individual samples; the expanded script also reruns the -read/divide/round controls. The earlier division table above refers to its original -report, rather than mixing results from separate runs. - -These are single-client end-to-end samples, including query compilation, database -work, row transport and Decimal decoding. They are not latency percentiles, -concurrency measurements, or estimates for large coefficients/high scales. -No other ORM test/build workload ran during measurement. +## Verify precision and measure cost + +Use the repository's decimal, division/window and average suites to verify +storage, arithmetic, rounding, catalog drift and rollback with real SQLite and +PostgreSQL connections. The Chrome checks cover exact Decimal transport; they do +not rerun the complete native precision suite. Android acceptance currently has +no Decimal-specific scenario. MySQL/MariaDB arithmetic limits are described in +[the engine guide](https://github.com/medz/dart-orm/blob/main/doc/mysql.md). + +The repository benchmark fetches and decodes 10000 integer-valued Decimal rows, +with one warmup and three measured runs per case. It includes ordinary reads, +division, rounding, average and running aggregates. Build before measuring: ```sh dart build cli --target=tool/benchmark_decimal.dart --output=/tmp/orm-decimal-benchmark -/tmp/orm-decimal-benchmark/bundle/bin/benchmark_decimal > /tmp/decimal-division.json +mkdir -p .dart_tool/benchmarks +/tmp/orm-decimal-benchmark/bundle/bin/benchmark_decimal > .dart_tool/benchmarks/decimal.json ``` -Set `ORM_TEST_POSTGRES` to an accessible disposable local PostgreSQL URL to include -PostgreSQL. The script disables TLS for that local connection, creates a uniquely -named schema and drops its schema afterward; the account needs schema-creation -permission. Without the variable it measures SQLite only. Run separately from -tests and builds. Compilation happens before timing. +Set `ORM_TEST_POSTGRES` to a disposable local PostgreSQL URL to include that +backend. The script disables TLS for this local benchmark, creates a uniquely +named schema and removes it afterward; the account needs schema-creation +permission. Without the variable it measures SQLite only. + +These are single-client end-to-end samples, including compilation of each query, +database work, transport and decoding. They are not latency percentiles, +concurrency measurements or estimates for large coefficients and high scales. +Run separately from tests and builds. Retain database/SDK versions and each sample +from the JSON report when comparing revisions. diff --git a/doc/defaults.md b/doc/defaults.md index 23f2ce9d..4f43b14c 100644 --- a/doc/defaults.md +++ b/doc/defaults.md @@ -78,5 +78,5 @@ considers existing rowids. Coordinate uniqueness when mixing these sources. `DateTime.now` is application time encoded by the UTC instant codec, not SQL CURRENT_TIMESTAMP. Ordinary Dart constructor defaults do not declare database -defaults. Use [computed columns](computed.md) for expressions the database +defaults. Use [computed columns](https://github.com/medz/dart-orm/blob/main/doc/computed.md) for expressions the database recomputes when their source values change. diff --git a/doc/execution.md b/doc/execution.md index 8c0688fa..e4f676e6 100644 --- a/doc/execution.md +++ b/doc/execution.md @@ -195,7 +195,7 @@ Fetch events include actual batch row counts; they carry the original query SQL for attribution. Parameters are counted but their values are not recorded. Optional `onAcquire` and `onDecode` callbacks separately report lease acquisition -and synchronous ORM result processing. See [plans and observations](observability.md) +and synchronous ORM result processing. See [plans and observations](https://github.com/medz/dart-orm/blob/main/doc/observability.md) for their scopes, inheritance and limits, and non-executing `query.inspect()`. diff --git a/doc/flutter.md b/doc/flutter.md index 057e5d5e..a78e2108 100644 --- a/doc/flutter.md +++ b/doc/flutter.md @@ -1,59 +1,90 @@ -# Native Flutter - -The standalone runtime has a fresh [Android acceptance capture](../research/validation/standalone-flutter.json) -at `7a54553`: 4/17/11 checks pass across installation, AOT upgrade and process -restart. See the [current validation record](../research/validation/standalone-redesign.md). -The detailed historical capture below remains tied to its original revision. - -[`example/flutter`](../example/flutter/README.md) is an actual Android application -using generated ORM APIs and `SqliteOptions.file` in the app's private files -directory. SQLite runs in the native driver's worker isolate. The small Android -method channel supplies the directory, launch phase, API level and process ID; -all database operations and migration checks run in Dart through the public ORM. - -The current example statically imports fixed Dart migrations and their recorded -fingerprints. Migration JSON assets and runtime asset loading have been removed. -The current APK capture verifies this workflow at source `85c4e42`. - -The [captured report](../research/validation/flutter.json) records Flutter 3.47.4 -stable, Dart 3.13.3 and SQLite 3.53.4 on an Android API 35 arm64 emulator. It includes -the exact framework/engine revisions, APK hashes, source hashes and device build. - -| Installed application | Checks | Verified behavior | -| --- | --- | --- | -| Version code 1, debug APK | 4 | Empty database, initial migration, generated transactional inserts, close and read-only reopen | -| Version code 2, release AOT APK, `adb install -r` | 17 | Existing file/history, only the pending migration, live catalog, original values/microseconds, new default, generated relations, committed watch refresh, rollback without persistence/notification, typed patch, worker progress, native cancellation and reuse, application version and read-only reopen | -| Same version 2 APK after force-stop/relaunch | 11 | New process, retained database/rows/relations/history, no migration replay, initial watch, compatibility and read-only reopen | - -These are 32 assertions across three phases, not 32 independent platform scenarios. -The host independently checks installed APK version codes, distinct live process -IDs, an unchanged database path and identical final rows/history after restart. -The old physical `body` column remains in place when Dart exposes it as `text`. -The upgrade adds a defaulted `done` column and a comments table without dropping -or rebuilding the existing notes table. - -During the final captured release run, summing a recursive sequence of two million -integers took 335,844 microseconds. The main isolate's 16 ms timer advanced 20 -times and its Flutter animation listener advanced 12 times during that awaited SQL -operation. The report's `flutterFrames` field counts those animation frame -callbacks; it is not a raster timing or delivered-FPS measurement. This establishes -that database work does not occupy the UI isolate for the entire query. It does -not establish a frame-rate target or physical-device performance. - -![Restarted release application](../research/validation/flutter-android.png) - -Build and run commands are in the [example instructions](../example/flutter/README.md). -`tool/test_flutter.dart` requires a dedicated emulator and refuses an already -installed acceptance application. Both APKs use the example's debug signing key -to permit a real upgrade with retained data. Reports travel through bounded log -chunks; Android's Flutter log sink truncated larger chunks during development, -so the application uses 700-character payloads. The host rejects incomplete or -conflicting chunks and waits for the activity transition before its screenshot. - -This verifies Android arm64 on the recorded emulator. Physical devices, iOS and -macOS Flutter need their own acceptance runs. Process force-stop/relaunch does not -simulate sudden power loss. Watch refreshes cover writes made through the same -database instance; external writes still need [explicit invalidation](watch.md). -The [native Dart migration regression](../research/validation/dart-migrations.json) -and 19 scenarios in each Chrome JS/WASM run are separate evidence. The compiled -APK histories have fixed fingerprints and need no schema or migration JSON assets. +# Flutter integration + +Use `package:orm/sqlite.dart` for both native Flutter and Flutter Web. SQLite runs +in a background isolate on native platforms and a worker in the browser. Generated +models, queries, transactions and saved Dart migrations use the same public API. + +## Open and own a database + +Choose the storage location at the application boundary, then pass the database +or transaction session to the code that needs it: + +```dart +import 'package:orm/sqlite.dart'; + +Future> openDatabase({String? nativePath}) => sqlite( + SqliteOptions.persistent('notes', nativePath: nativePath), +); +``` + +Supply an application-owned file path on native platforms. The ORM does not choose +a documents directory or require a Flutter path plugin. The +[Android example](https://github.com/medz/dart-orm/blob/main/example/flutter/README.md) obtains its private files directory +through a small platform channel; applications can use their existing directory +provider. The browser uses the persistent name for origin-private file storage +(OPFS) and does not use the native path. + +Use `SqliteOptions.memory()` for temporary databases. A database handle owns its +connection and worker; keep it at the appropriate application or feature lifetime, +then await `close()` when that owner shuts down. Do not open a new handle for each +widget rebuild or retain a transaction session after its callback returns. + +## Bundle migrations + +Statically import the saved migration registry and apply its checked history +before issuing application queries: + +```dart +import 'package:orm/migrate.dart'; +import 'migrations/migrations.g.dart'; + +await Migrator(db.sql).apply(migrationHistory.checked); +``` + +Migration definitions and recorded fingerprints compile into the application. +They do not load JSON assets or import current model classes. Keep previously +shipped migration files immutable and add a reviewed migration for each physical +schema change. See [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md) for version compatibility, catalog +verification and recovery rules. + +## Build for the web + +Flutter Web bundles the package's matching worker and WASM resources automatically. +There is no separate ORM plugin or application asset declaration to maintain. +Normal Flutter commands work: + +```sh +flutter run -d chrome +flutter build web +flutter build web --wasm +``` + +Serve the result from a secure browser context. Persistent storage requires OPFS +and an exclusive owner; unsupported storage fails explicitly. Deployment base +paths, content security policy, advanced resource overrides and browser limits are +covered in [SQLite Web](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md). + +Hot reload retains application state. Release tests do not establish that Flutter +Web hot restart always releases abandoned JS resources. Close a live OPFS session +or refresh the page when restarting. Query subscriptions cover known committed +writes; another tab or independent connection needs [explicit invalidation](https://github.com/medz/dart-orm/blob/main/doc/watch.md). + +## Verify upgrades and lifecycle + +The repository [Flutter example](https://github.com/medz/dart-orm/blob/main/example/flutter/README.md) includes native and +browser runners. Its Android workflow installs a legacy debug APK, upgrades it to +a release AOT APK with retained application data, then force-stops and reopens it +in another process. Assertions cover fixed migration history, preserved rows, +new defaults and relationships, generated writes, rollback, subscriptions, +read-only reopening, worker progress and supported cancellation. + +Run `tool/test_flutter.dart` with a dedicated emulator and the two built APKs as +shown in the example instructions. It refuses an existing acceptance installation +and writes reports, logs and a screenshot under `.dart_tool/flutter/`. The +Web runner is `dart run tool/test_flutter_web.dart /absolute/path/to/flutter`. + +Animation and timer progress while SQL is running checks worker separation. It +is not a frame-rate measurement. Emulator process restart does not simulate +power loss, and Android checks do not certify iOS, macOS Flutter or physical +devices. [Progress](https://github.com/medz/dart-orm/blob/main/doc/progress.md) records the tested revision and current validation +status; [capabilities](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md) describes driver-specific limits. diff --git a/doc/generation.md b/doc/generation.md index ca0651dd..29debd22 100644 --- a/doc/generation.md +++ b/doc/generation.md @@ -11,7 +11,7 @@ and an immutable `appSchema`; entity declaration values remain in the source library. Import the generated client and the chosen driver in application code. Programmatic generation is in `generate.dart`; build_runner factories are exported only by `builder.dart`. -See [API boundaries](api.md). +See [API boundaries](https://github.com/medz/dart-orm/blob/main/doc/api.md). Generation validates declaration structure and generated symbol names. Database member collisions such as an entity called `close` fail here; keep the physical @@ -29,7 +29,7 @@ dart run orm generate lib/schema.dart Initialization creates typed `orm.config.dart`, a model and static migration history without connecting. The path-free command uses `OrmConfig.schema` (or -`lib/schema.dart` when no config exists). See [project CLI](cli.md). +`lib/schema.dart` when no config exists). See [project CLI](https://github.com/medz/dart-orm/blob/main/doc/cli.md). This writes `lib/schema.orm.dart` and `lib/schema.snapshot.dart`. The latter is the physical schema snapshot used by migration tools. It is a standalone Dart library @@ -118,125 +118,55 @@ evaluation erases an extension type's representation. Generated source stays reviewable and can be committed with the application. Schema snapshot changes are migration inputs, not permission to apply DDL -automatically. Follow the [migration workflow](migrations.md) to review and apply +automatically. Follow the [migration workflow](https://github.com/medz/dart-orm/blob/main/doc/migrations.md) to review and apply database changes. -## Measurements and verification +## Verify generated code -Run the reproducible consumer-project benchmark from the ORM repository: +Run `dart analyze` after generation so changes to a model, selector or result type +are checked in application code. A renamed Dart field can retain its physical +column with `@ColumnName('old_name')`; changing a database name requires an explicit +[reviewed migration](https://github.com/medz/dart-orm/blob/main/doc/migrations.md). + +The generator reports ORM-specific errors such as a computed expression in a key +selector. These can be valid Dart expressions, so editor diagnostics alone do not +replace a generation run. Keep `build_runner watch` active while editing schemas. +Treat generated files as outputs: make edits in the declaration, regenerate, then +repair affected application references with the type checker. + +## Measure generation and editor cost + +From an ORM repository checkout: ```sh dart run tool/benchmark_generation.dart +dart run tool/benchmark_editor.dart --smoke +dart run tool/benchmark_editor.dart ``` -It creates disposable projects containing 10, 100 and 1000 four-field record -models. For each, it measures the first build, an unchanged build, watcher startup, -a field edit, an imported metadata edit and static analysis. It also verifies -that an unrelated edit leaves the watcher idle and output timestamps unchanged. -Its [JSON report](../research/benchmarks/generation.json) records SDK/package -versions, hardware, source/output sizes and measurement conditions. - -These are one-sample development-machine observations. Dependency download is -excluded, shared SDK/pub/native caches are warm, and process/watch startup costs -are included where applicable. They are not p50/p95 results or IDE completion -measurements. The first build includes build_runner's builder compilation. Run -the benchmark independently of test suites or other builds, with the generator -source fixed for the entire run. - -Historical measurements before the Dart snapshot workflow, recorded on Apple M3 Max with Dart 3.13.3 and build_runner 2.16.1 (seconds): - -| Models | First build | Unchanged build | Watch field edit | Watch import edit | Analyze | -| --- | ---: | ---: | ---: | ---: | ---: | -| 10 | 16.20 | 0.57 | 0.52 | 0.40 | 0.48 | -| 100 | 16.26 | 0.57 | 0.65 | 0.66 | 0.50 | -| 1000 | 17.96 | 0.58 | 2.17 | 2.22 | 3.48 | - -All three unchanged builds wrote zero outputs. Unrelated edits stayed idle during -the one-second observation window. The 1000-model client contains 2,343,598 bytes -after the edit; its result shape is a single schema library with four fields per -model and one added field on the first model. - -Tests compare builder and CLI output, validate in-memory assets and prior-builder -outputs, and run real build/watch processes for dependency changes, failure -recovery and output cleanup. Static analysis of the generated consumer project -is part of that process test. - -## Editor completion, diagnostics and rename - -Run `dart run tool/benchmark_editor.dart` to exercise the installed Dart Analysis -Server over stdio LSP in disposable consumer projects. `--smoke` checks ten models -with two warm samples and writes only under `.dart_tool/`. The full run writes -[`editor.json`](../research/benchmarks/editor.json), including the runtime commit, -harness hashes, raw samples, diagnostic ranges, rename edits and resulting source. -The protocol is documented by the [Dart 3.13.3 SDK](https://github.com/dart-lang/sdk/blob/3.13.3/pkg/analysis_server/tool/lsp_spec/README.md). - -On Apple M3 Max / Dart 3.13.3, warm completion p50 / p95 in **milliseconds**: - -| Models | Table entries | Schema Record fields | Query fields | Create arguments | Projected Record fields | -| --- | ---: | ---: | ---: | ---: | ---: | -| 10 | 0.809 / 1.117 | 0.439 / 0.511 | 0.484 / 1.126 | 0.238 / 0.293 | 0.518 / 0.590 | -| 100 | 1.497 / 2.812 | 0.428 / 0.492 | 0.484 / 0.559 | 0.250 / 0.315 | 0.568 / 0.600 | -| 1000 | 11.572 / 17.094 | 0.705 / 0.788 | 0.780 / 0.850 | 0.226 / 0.307 | 1.330 / 2.863 | - -Each cell uses twenty unchanged-document requests after two unrecorded warmups; -quantiles use nearest rank. These numbers include client transport/JSON handling -but exclude editor UI/plugin rendering. Dependency installation is offline and -outside timing; shared SDK/pub/OS caches are not cleared. They are local protocol -measurements, not a ranking against another ORM or declaration frontend. - -Warm completion excludes opening/editing and analysis readiness. Initial server -startup plus project analysis took 0.311 / 0.487 / 3.405 seconds respectively. At -1000 models, the query-field probe waited 128.488 ms for a diagnostic after opening -the incomplete expression, then its first completion took 4.109 ms. The schema -field probe waited 165.844 ms. The report separates these observations from warm -samples; they must not be described as sub-millisecond edit-to-suggestion latency. - -All fifteen probe shapes returned their required labels. A two-field named Record -projection offered `id` and `title` but did not offer the unselected `score` or -`status`. Each scale also checked four deliberate type errors: create input, -predicate value, scalar result assignment and access to an unselected field. Each -diagnostic must match both its code and the current token's exact source range. -Changes are serialized because this SDK omits diagnostic document versions. A -fresh diagnostic clears the repaired client error. A separate CLI analysis checks -the final edited consumer, rather than treating an absent notification as success. - -The language-symbol rename results are the same at all three scales: - -| Declaration | Observed result | -| --- | --- | -| Named Record field | Both prepare and rename return `null`; unavailable | -| Primary-constructor class field | Declaration and typed member reference updated | -| Class-based table field | Dart field and typed reference updated; SQL column string retained | -| Record typedef name | Declaration and both type references updated; separate Record alias retained | - -These are language-symbol probes. They do not compare three ORM authoring -frontends generating identical APIs. Renames use a fresh server after the error -probes. A subsequent [recovery capture](../research/validation/editor-recovery.json) -reproduces the same-session failure using `dart run tool/benchmark_editor.dart ---same-session`: at ten models, five completion probes, four exact-code/range -diagnostics and their repair finish, then `textDocument/rename` for the primary -constructor field times out after 60 seconds. The process exits nonzero before -attempting the larger scales; the [original log](../research/validation/editor-recovery.log) -is retained. `--smoke` on the same harness restarts the server before rename and -completes the class/table/alias edits plus final analysis. Server PIDs and source -hashes are recorded. The underlying SDK/harness interaction is not diagnosed; -this is a reproducible limitation with a checked restart path, not general IDE -reliability or a claim that the SDK alone caused it. - -Record field edits therefore require reviewing declaration/selector references, -regenerating, and repairing application references with the type checker. Do not -edit generated files to perform a schema rename. To change only a Dart field name, -keep its physical column explicit, for example -`@ColumnName('name') String displayName`. A physical database rename still needs -the reviewed [migration workflow](migrations.md). - -ORM selector rules are a separate diagnostic boundary: the valid Dart expression -`rows0.index((r) => r.id + 1)` passes CLI analysis but fails generation with a -direct-field-selector error and source offset. Stock LSP does not supply that ORM -diagnostic. Keep generation/watch running for schema rule validation. - -The separate [declaration-form experiment](authoring.md) now compares Record, -primary-constructor and table-class inputs for the same four related models. -It checks identical generated APIs/snapshots across field edits and renames, -stale-client type errors, schema refactor propagation and original error locations. -Its class/table adapters are experimental tools, not supported package entrypoints. +The generation benchmark creates disposable projects with 10, 100 and 1000 +four-field Record models. It measures first and unchanged builds, watcher startup, +field and imported-metadata edits, and analysis. It also checks that an unrelated +edit leaves output timestamps unchanged. The report is written to +`.dart_tool/benchmarks/generation.json`; a positional argument chooses another +report path. + +The editor benchmark drives the installed Dart Analysis Server over stdio LSP. +It checks completion labels, deliberate type errors with exact diagnostic ranges, +source edits and a final consumer analysis. Full results go to +`.dart_tool/benchmarks/editor.json`. `--smoke` uses ten models and two warm samples, +writing `.dart_tool/benchmarks/editor-smoke.json`; it checks the harness rather +than establishing performance. `--same-session` retains the server across error +recovery and rename probes; its separate report includes `-recovery` in the name. + +Generation timing includes process startup where applicable, excludes dependency +downloads, and uses warm shared SDK/pub caches. Editor measurements include protocol +transport and JSON processing, but exclude GUI rendering and editor extensions. +Keep startup, analysis readiness, first completion and warm completion separate. +A successful rename probe does not guarantee every IDE workflow; Record field +renaming depends on the Dart SDK. Always regenerate and analyze after refactoring. + +Run these tools independently of other builds with source and SDK versions fixed. +Read sample counts, conditions and errors in the produced report before comparing +results. The build/watch regression suites also exercise imported metadata, +prior-builder output, error recovery and generated-file cleanup. diff --git a/doc/importing.md b/doc/importing.md index 814ce86c..dfc08ae9 100644 --- a/doc/importing.md +++ b/doc/importing.md @@ -22,7 +22,7 @@ dart run bin/migrate.dart verify ``` Use `--dialect postgres`, `mysql` or `mariadb` for the matching server's registry. -Configure the same database in the [Dart migration entrypoint](migrations.md). Existing database-specific defaults and objects need reviewed migrations; +Configure the same database in the [Dart migration entrypoint](https://github.com/medz/dart-orm/blob/main/doc/migrations.md). Existing database-specific defaults and objects need reviewed migrations; an imported declaration is not automatically a portable creation script for the other dialect. Baseline verifies the declared schema before recording history and does not execute the initial creation SQL. @@ -63,9 +63,9 @@ indexes and representable foreign keys become declarations. Relationships get na and inverse selections; rename these before generation if a domain name is clearer. Foreign-key actions include SET DEFAULT on supporting engines; MySQL/MariaDB reject it. Unsupported unsigned and binary storage must be reviewed explicitly; -see [MySQL/MariaDB limits](mysql.md). +see [MySQL/MariaDB limits](https://github.com/medz/dart-orm/blob/main/doc/mysql.md). -Ordinary enforced [CHECK constraints](checks.md) become `.check(...)` declarations +Ordinary enforced [CHECK constraints](https://github.com/medz/dart-orm/blob/main/doc/checks.md) become `.check(...)` declarations with their native names and SQL. A nonblocking `IMPORT.CHECK_SQL` note requests review of backend-specific expressions before deployment on another dialect. PostgreSQL unvalidated, unenforced and inheritance-specific checks remain unmanaged. @@ -111,10 +111,10 @@ existing NaN or Infinity values will fail typed decoding. SQLite requires the recognized column collation to establish decimal storage and recognizes only the emitted precision CHECK. Varchar, arrays, domains and other native types still need explicit support. See -[decimal boundaries](decimals.md) before reviewing a draft. +[decimal boundaries](https://github.com/medz/dart-orm/blob/main/doc/decimals.md) before reviewing a draft. SQLite rowid integer primary keys and PostgreSQL BY DEFAULT integer primary-key -identities get `@Id.generated()`. [Computed expressions](computed.md) become +identities get `@Id.generated()`. [Computed expressions](https://github.com/medz/dart-orm/blob/main/doc/computed.md) become `@Computed.sql` with their stored/virtual mode and an `IMPORT.COMPUTED_SQL` portability review note. ALWAYS identities, non-primary identities and nullable primary keys require explicit write semantics @@ -129,7 +129,7 @@ receive a distinct Dart name while retaining the physical SQL name. PostgreSQL SMALLINT/INTEGER columns and SQLite INTEGER columns with recognized ORM range checks retain their `@IntegerBits(16)` or `@IntegerBits(32)` declaration. -See [integer widths](types.md#signed-integer-column-widths) for range enforcement, +See [integer widths](https://github.com/medz/dart-orm/blob/main/doc/types.md#signed-integer-column-widths) for range enforcement, aggregation and migration behavior. SQLite import requires 3.37 or later to distinguish ordinary, virtual and shadow diff --git a/doc/migrations.md b/doc/migrations.md index 2e184bad..460d90c9 100644 --- a/doc/migrations.md +++ b/doc/migrations.md @@ -18,7 +18,7 @@ dart run orm migrate inspect tasks `init` also accepts `postgres`, `mysql` and `mariadb`. It generates a nominal model, client, physical snapshot, empty static registry and `orm.config.dart`. Existing files are never replaced, and initialization does not connect or execute DDL. -See the [project CLI](cli.md) for the generated typed configuration and all options. +See the [project CLI](https://github.com/medz/dart-orm/blob/main/doc/cli.md) for the generated typed configuration and all options. The config is an explicit Dart entrypoint with `runOrmCli(args, config: OrmConfig(...))`. It statically imports its current snapshot and migration history. Connection @@ -136,7 +136,7 @@ ordinary SQLite column values with a computed expression. Computed-to-ordinary SQLite changes materialize the existing result instead. Neither offline checking nor generation applies them. -Start with [catalog import](importing.md) for an existing database, then run +Start with [catalog import](https://github.com/medz/dart-orm/blob/main/doc/importing.md) for an existing database, then run `baseline` to verify and register its final historical schema without replaying creation SQL. Add `--json` for machine reports (not migration artifacts). The lower-level `runMigrationCli` defaults to JSON reports. Exit code 2 indicates catalog drift, 64 invalid CLI arguments, and 1 an execution failure. @@ -170,7 +170,7 @@ lock acquisition uses short `pg_try_advisory_lock` queries outside a transaction with `Migrator(db, lockTimeout: ...)` defaulting to 30 seconds. Waiting does not hold an old SQL snapshot that could deadlock concurrent index creation. MySQL/MariaDB DDL commits independently and uses exact catalog preconditions, -postconditions and durable checkpoints; see [MySQL/MariaDB migrations](mysql-migrations.md). +postconditions and durable checkpoints; see [MySQL/MariaDB migrations](https://github.com/medz/dart-orm/blob/main/doc/mysql-migrations.md). Migration planning checks applied history and checksums. Catalog verification is an explicit separate operation; SQL history alone does not prove schema equality. @@ -252,12 +252,12 @@ rejected downgrade does not delete or rebuild tables. - Type changes require SQL conversion expressions for the selected dialect. Expressions are trusted migration code and use column names after declared renames. -[Computed target columns](computed.md) use their own expressions to populate old +[Computed target columns](https://github.com/medz/dart-orm/blob/main/doc/computed.md) use their own expressions to populate old rows and derive values on type changes. They are excluded from rebuild copy maps; their addition does not require a default or application backfill. Mode conversions and native expression dependencies have separate documented migration limits. -For long data transformations, use a reviewed [resumable backfill](backfills.md). +For long data transformations, use a reviewed [resumable backfill](https://github.com/medz/dart-orm/blob/main/doc/backfills.md). It carries a historical table definition, commits bounded batches with their primary-key cursors, supports bounded runs, and verifies completion. Both SQLite and PostgreSQL support it; general `CheckedSql` autocommit operations still require @@ -290,7 +290,7 @@ they are never silently discarded. Removing columns from a table with triggers also requires explicit manual handling. A new managed index cannot silently replace an unmanaged index of the same name. -[Declared CHECK constraints](checks.md) participate in schema diffs, inspection +[Declared CHECK constraints](https://github.com/medz/dart-orm/blob/main/doc/checks.md) participate in schema diffs, inspection and import. SQLite adds/changes/removes them through a rebuild; PostgreSQL uses constraint DDL. Checked renames explicitly remove and re-add the declared SQL, which can require two SQLite copies. Existing rows are validated before commit. diff --git a/doc/mysql-migrations.md b/doc/mysql-migrations.md index 8c559ca4..acec4861 100644 --- a/doc/mysql-migrations.md +++ b/doc/mysql-migrations.md @@ -18,7 +18,7 @@ dart run orm migrate verify `init --database mariadb` selects MariaDB instead. Models, physical snapshots, reviewed migration operations and the static registry are all Dart source. A saved migration contains SQL for its selected engine only; current application models -are not imported by historical migration files. See [migrations](migrations.md) +are not imported by historical migration files. See [migrations](https://github.com/medz/dart-orm/blob/main/doc/migrations.md) for configuration, fingerprints, deployment bundles, baseline and version checks. ## DDL and recovery diff --git a/doc/observability.md b/doc/observability.md index 6f803c6c..636550ab 100644 --- a/doc/observability.md +++ b/doc/observability.md @@ -115,4 +115,4 @@ fail after an autocommitted write; observation does not move transaction boundar Real backend and browser coverage checks event scopes, query/row counts, acquisition waiting/cancellation and observer failures. Runtime throughput and -allocation comparisons remain a separate [acceptance gate](acceptance.md). +allocation comparisons remain a separate [acceptance gate](https://github.com/medz/dart-orm/blob/main/doc/acceptance.md). diff --git a/doc/performance.md b/doc/performance.md index 289572a9..c8b5eebc 100644 --- a/doc/performance.md +++ b/doc/performance.md @@ -9,11 +9,10 @@ ORM_TEST_POSTGRES='postgresql://localhost/orm_bench' dart run tool/benchmark_run The PostgreSQL account needs permission to create/drop its own temporary schema. The tool removes that schema and its temporary SQLite WAL file when finished. It does not alter the application's tables. `--smoke` uses five timing samples -and writes `.dart_tool/runtime-smoke.json`; it validates the harness and is not -a performance result. The normal run records -[`research/benchmarks/runtime.json`](../research/benchmarks/runtime.json). -Use `--output ` to retain a subsequent measurement separately from that -baseline, for example `--output research/benchmarks/runtime-selection.json`. +and writes `.dart_tool/benchmarks/runtime-smoke.json`; it validates the harness and is not +a performance result. The normal run writes `.dart_tool/benchmarks/runtime.json`. +Use `--output ` to retain separate runs, for example +`--output .dart_tool/benchmarks/runtime-after.json`. ## Comparison contract @@ -31,11 +30,11 @@ count; this baseline is handwritten for that workload, not a general replacement for the ORM query planner. Raw SQL, parameter values, statement count and complete business results are checked against the corresponding generated ORM query. Independent assertions check root count, per-parent ordering/limits and grouped -counts. All data belongs to the generated [example schema](../example/schema.dart). +counts. All data belongs to the generated [example schema](https://github.com/medz/dart-orm/blob/main/example/schema.dart). | Case | Result | Expected SQL result volume | | --- | --- | --- | -| `full_rows` | 100 complete User Records; nullable, long Unicode nicknames | 100 rows, one statement | +| `full_rows` | 100 complete user results; nullable, long Unicode nicknames | 100 rows, one statement | | `two_columns` | 100 `(id, email)` Records | 100 rows, one statement | | `three_posts` | 100 users with their latest three `(id, title)` posts | 100 parent + 300 child rows, two statements | | `aggregate` | Counts for ten score groups | 10 rows, one statement | @@ -116,131 +115,30 @@ not an exact census of every allocation or a measurement of total allocated byte Tracing can deoptimize code and changes execution cost; no traced durations are used as normal throughput results. -## Interpretation - -Captured on 2026-09-15 with an Apple M3 Max, Dart 3.13.3, SQLite 3.53.4 and -PostgreSQL 18.4. Runtime source is `94d24fe`. Durations below are milliseconds; -throughput is operations/second for the separate eight-client run. - -| Environment | Case | Raw p50 | ORM p50 | ORM p95 | ORM concurrent ops/s | -| --- | --- | ---: | ---: | ---: | ---: | -| SQLite WAL | full rows | 0.111 | 0.160 | 0.242 | 6,919 | -| SQLite WAL | two columns | 0.055 | 0.075 | 0.127 | 14,159 | -| SQLite WAL | three posts | 0.728 | 0.817 | 0.915 | 1,238 | -| SQLite WAL | aggregate | 0.036 | 0.040 | 0.049 | 24,564 | -| PostgreSQL loopback | full rows | 0.931 | 0.994 | 1.126 | 1,838 | -| PostgreSQL loopback | two columns | 0.530 | 0.555 | 0.849 | 2,817 | -| PostgreSQL loopback | three posts | 1.778 | 1.960 | 2.383 | 725 | -| PostgreSQL loopback | aggregate | 0.352 | 0.365 | 0.406 | 5,956 | -| PostgreSQL delayed relay | full rows | 82.706 | 83.526 | 86.316 | 46 | -| PostgreSQL delayed relay | two columns | 82.083 | 81.652 | 85.104 | 47 | -| PostgreSQL delayed relay | three posts | 166.178 | 168.265 | 173.045 | 23 | -| PostgreSQL delayed relay | aggregate | 80.660 | 80.563 | 83.713 | 49 | - -The delayed relay's actual echo RTT p50 was **26.410 ms**, despite the nominal -20 ms delay; timers and scheduling add overhead. Full-driver `SELECT 1` p50 was -82.678 ms. Loopback echo p50 was 0.188 ms. Small reversals where ORM appears faster -under the delayed relay are not evidence of an ORM speedup. - -For PostgreSQL, selecting two columns instead of complete users reduced received -protocol bytes from **27,784 to 4,693** in the probe. Batched relationships returned -100 parent and 300 child rows in two SQL statements. The ORM's separate eight-client -relation probe measured acquisition p50 of 5.530 ms for SQLite, 5.212 ms for local -PostgreSQL and 180.287 ms with the delayed relay. Pool saturation and network costs -matter more than a single idle-connection probe suggests. - -Across three SQLite relationship reads, selected traces recorded **996 raw versus -8,244 ORM List/backing-List allocations** (`_List` plus `_GrowableList`). These are -main-isolate traces with tracing enabled, not all-process allocated bytes. They -identify intermediate collection construction for follow-up review. The measured -local ORM p50 overhead in that case was 0.089 ms. A future optimization should -re-run this harness and preserve exact SQL, results, transaction behavior and -observer semantics before claiming improvement. - -The captured report identifies runtime source commit, SDK/OS/CPU, database versions, -fixture, sample counts and harness hashes. It is one machine/run, without confidence -intervals or a cross-library comparison. It does not establish AOT, browser or Flutter -performance. Correctness checks for those platforms have separate evidence. +## Interpret a comparison + +Compare exact SQL, bound parameters, statement counts and complete business +results before comparing durations. A faster run with fewer returned rows or a +different transaction boundary is a different workload. Use absolute overhead, selected bytes, actual statement counts and measured lease -waits together. A small local query can have a large percentage overhead while -remaining short in absolute time. On a delayed connection, protocol round trips -can dominate; fewer selected columns do not eliminate those round trips. Allocation -traces identify work worth investigating but do not by themselves justify a cache -or a new abstraction. - -## Direct selection decoding - -The separate [selection report](../research/benchmarks/runtime-selection.json) -captures runtime `d0a8c9a` on the same machine, SDK and database versions. Field -readers now pass values directly to typed mappers or the requested dynamic Map, -avoiding a temporary values List for each row. Relationship expansion allocates -one fixed-length buffer and copies the original row into it. This preserves -driver-row ownership and adds no cache or dependency. - -The workload, relay and VM-profiling helper hashes match the baseline. The main -harness only adds output-path selection, allowing both reports to be retained. -Each of the twelve scenario/case combinations has 200 latency and concurrent -samples per lane and 64 separate acquisition samples. Independent report readback -confirms identical SQL, parameters, statement/row counts and logical/protocol -bytes between raw/ORM and before/after. Temporary PostgreSQL schemas were removed. - -The selected main-isolate List traces over **three reads** are: - -| Backend | Case | Raw before → after | ORM before → after | ORM reduction | -| --- | --- | ---: | ---: | ---: | -| SQLite WAL | full rows | 39 → 40 | 1,239 → 333 | 73.1% | -| SQLite WAL | two columns | 37 → 37 | 885 → 282 | 68.1% | -| SQLite WAL | three posts | 996 → 996 | 8,244 → 5,220 | 36.7% | -| SQLite WAL | aggregate | 29 → 29 | 371 → 308 | 17.0% | -| PostgreSQL loopback | full rows | 3,386 → 3,382 | 4,598 → 3,695 | 19.6% | -| PostgreSQL loopback | two columns | 2,359 → 2,359 | 3,215 → 2,611 | 18.8% | -| PostgreSQL loopback | three posts | 10,235 → 10,248 | 17,564 → 14,541 | 17.2% | -| PostgreSQL loopback | aggregate | 451 → 451 | 803 → 740 | 7.8% | - -Counts sum `_List` and `_GrowableList`, including backing arrays. All captured -traces report zero truncated stacks; the selected-class and isolate limitations -above still apply. These percentages describe observed List creations, not total -allocated bytes, retained heap, RSS or throughput. - -Normal untraced local latency did **not** establish a speed improvement. Several -raw-driver timings increased too. All local cases are shown in milliseconds: - -| Backend | Case | Raw p50 before → after | ORM p50 before → after | New ORM p95 | -| --- | --- | ---: | ---: | ---: | -| SQLite WAL | full rows | 0.111 → 0.127 | 0.160 → 0.170 | 0.267 | -| SQLite WAL | two columns | 0.055 → 0.057 | 0.075 → 0.075 | 0.164 | -| SQLite WAL | three posts | 0.728 → 0.806 | 0.817 → 0.907 | 1.099 | -| SQLite WAL | aggregate | 0.036 → 0.036 | 0.040 → 0.041 | 0.053 | -| PostgreSQL loopback | full rows | 0.931 → 0.907 | 0.994 → 0.979 | 1.189 | -| PostgreSQL loopback | two columns | 0.530 → 0.553 | 0.555 → 0.593 | 0.827 | -| PostgreSQL loopback | three posts | 1.778 → 1.909 | 1.960 → 2.154 | 2.498 | -| PostgreSQL loopback | aggregate | 0.352 → 0.353 | 0.365 → 0.375 | 0.510 | - -Local ORM eight-client throughput was also lower in this capture; all durations -and throughput samples are retained in the report. Two separate captures cannot -distinguish small runtime regressions from load, scheduling and JIT variability. -The supported conclusion is fewer intermediate List allocations, with unchanged -SQL/result volume and verified correctness, not a latency or throughput gain. - -The delayed relay's echo p50 changed from 26.410 to 24.725 ms and full-driver -`SELECT 1` from 82.678 to 75.129 ms. Relationship raw/ORM p50 changed from -166.178/168.265 to 162.096/162.396 ms. Transport calibration itself changed, so -those lower durations cannot be attributed to selection decoding. - -The [PostgreSQL adapter](../lib/postgres.dart) still prepares each statement, -consumes its bound ResultStream and awaits disposal. Inspection of the locked -`postgres` 3.5.12 implementation shows separate awaited parse and close messages; -`_PreparedStatement` also queues transaction portals for cleanup in `run()` or -`dispose()`. Retaining statements while continuing to use `bind()` would retain -that pending cleanup until disposal. Simply removing disposal is therefore not -a valid optimization. The adapter currently owns cancellation and awaits control -cleanup before allowing reuse. A future connection-scoped statement cache or -protocol change must preserve these lifecycle guarantees and be evaluated with -the delayed-relay harness. No implicit statement cache is introduced here. - -Correctness after this change: 813 native tests with PostgreSQL enabled, clean -static analysis, and 18 real Chrome scenarios for each of JS and Dart WASM. The -browser membership scenario additionally covers six mixed field types and -deferred, ordered mapping. These remain separate from performance evidence and -do not establish native Flutter behavior. +waits together. A short local query can have a large percentage overhead while +remaining short in absolute time. With network delay, protocol round trips can +dominate; selecting fewer columns does not eliminate those round trips. Allocation +traces can locate intermediate collections, but do not establish retained memory +or application throughput. + +Retain the report's runtime revision, SDK, OS/CPU, database versions, fixture, +sample counts and harness hashes. Repeat runs to assess load, scheduling and JIT +variation. If raw-driver timings or calibrated relay latency also change, do not +attribute the entire difference to the ORM. A native JIT result says nothing about +AOT, browser or Flutter performance without a corresponding workload there. + +Typed selection decodes values into the requested scalar, Record, DTO or dynamic +Map. Relationship expansion preserves driver-row ownership. Any optimization must +retain mapping order, decoder failures, transaction boundaries, cursor cleanup +and observation events. Statement reuse must also preserve protocol disposal and +connection ownership; omitting cleanup is not a valid cache. + +See [observability](https://github.com/medz/dart-orm/blob/main/doc/observability.md) for the event timing scopes and +[acceptance](https://github.com/medz/dart-orm/blob/main/doc/acceptance.md) for correctness checks that accompany performance work. diff --git a/doc/progress.md b/doc/progress.md index c4d5628a..de6255cb 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -1,950 +1,78 @@ # Implementation status -## Beta 1 release preparation - -Version `6.0.0-beta.1` introduces the new implementation after the published 5.3.4 -line. README onboarding, the `doc/` directory, package metadata, changelog and -archive exclusions are prepared for publication. The build_runner registration -now advertises its actual Dart outputs rather than the retired JSON names. -The independent consumer quickstart passes initialization, Dart migration creation, -check/apply/verify, analysis, query projection and a transaction. The real -build_runner build/watch regression and package analysis pass. Publication -preflight validates the archive; pub.dev and the GitHub prerelease provide the -final publication state. The SQLite Web build includes package metadata in its -fingerprint; the beta version therefore regenerates the worker and manifest -without changing database execution logic. - -## Standalone ORM redesign - -Merged implementation: [PR #485](https://github.com/medz/dart-orm/pull/485), -`a3fef800` on `main`. The delivery scope is SQLite (native, -Dart Web and Flutter Web), PostgreSQL, MySQL 8.4 and MariaDB 11.8. The user has -authorized commits, push, a Codex-reviewed PR and merge after verification. - -Independent values, metadata, driver, raw runtime and typed SQL entrypoints are -implemented. Generated clients use the SQL layer; the ORM composes a raw -`SqlDatabase`. Nominal Dart model classes, four-dialect generation, engine-specific -migrations and the typed project CLI are implemented. Raw migration/catalog -operations now use `.sql` when starting from an ORM database. - -The first complete native regression invocation passed 1119 tests and found five -failures in older CLI output expectations and new temporary-metadata tests. All -five have been corrected. The final CLI/import/plan/MySQL migration and SQL review -run passes 71 tests; the session/transaction/stream/retry/acquisition/watch run -passes 282 tests, including 18 new borrowed-connection lifecycle regressions. -A further 176-test PostgreSQL review run passes after bounding initial backend -PID discovery; four real TCP-proxy regressions reproduce the original hang and -verify timeout/cancellation before any application write. Forty MySQL driver and -migration-target checks pass after aligning the minimum MySQL version to 8.4. -MySQL/MariaDB per-operation timeouts are separately advertised from cancellation; -140 real driver/database/transaction checks pass. Timeout closes the driver and -may leave a submitted write outcome unknown; transaction/retry gates stay strict. -Static analysis is clean. Registry engine parsing, non-atomic plan reporting, -temporary-history shadowing and session ownership were corrected during review. - -Fresh Chrome JS/WASM and Flutter Web JS/WASM (with and without isolation) each -pass 21 scenarios. Android debug installation, AOT release upgrade and independent -process reopen pass 4/17/11 checks. Both APKs exclude Web resources. Root and -Flutter analysis pass. [The final local acceptance record](../research/validation/standalone-redesign.md) -pins the runtime source and limits. Final GitHub CI and Codex review passed for the merged head; -earlier captures below retain their revisions. - -Linux CI exposed native-asset packaging and capability assumptions in tests. -The complete run subsequently found 17 remaining interruption assumptions in -transaction/acquisition/watch tests (1129 passed, 25 skipped); those test cases -now cover supported behavior or explicit rejection without changing runtime. -The import AOT fixture now uses a complete `dart build cli` bundle; borrowed -PostgreSQL tests include URL passwords (48 related checks pass). SQLite -interruption-dependent cases honor the actual build capability; default Linux -`sqlite3` 3.6.0 rejects cancellation/deadlines and bounded retries before SQL. -This limitation is documented rather than replaced with a second SQLite library. -The complete Linux native stage now passes 1136 tests with 41 capability skips. -Chrome JS passes 21 scenarios; an exiting-process profile cleanup race in the -runner is corrected with bounded retries. Browser checks run in an independent CI -job. The final [CI run](https://github.com/medz/dart-orm/actions/runs/35457956149) -passes 1141 native tests with 41 capability skips and 21 scenarios each for Chrome -JS/WASM. Codex reviewed the exact final head without further findings, and PR #485 -was merged with an identical source tree. - -Design: [standalone redesign](../research/standalone-orm-redesign.md). -Research: [Prisma v8 reassessment](../research/prisma-v8-reassessment.md). - -## Unified SQLite integration - -The current refactor uses `sqlite.dart` for native and browser connections. Shared -sqlite3 statement/cursor execution replaces the duplicated implementations. -`SqliteOptions.persistent(name, nativePath: ...)` keeps storage identity explicit; -Flutter Web automatically bundles its worker and WASM, and plain Dart exports the -same verified resources with `dart run orm web-assets`. Startup verifies worker -build/protocol identity and WASM integrity. Asset names include their content hash. - -The real Flutter release builds pass 21 scenarios each in JS, WASM without -isolation headers and WASM with isolation headers. Plain Dart JS/WASM also pass -21 each. The repository Flutter example builds for Web, resource export succeeds, -and static analysis is clean. The complete native suite passes **875 tests in -09:12**, with real SQLite/PostgreSQL enabled. Five focused native checks and all -Web configurations pass after finalizing the release artifacts. Negative startup -checks reject both an old protocol and a different build using the current -protocol. An Android arm64 asset bundle builds and excludes SQLite Web resources. -See [validation and limits](../research/validation/sqlite-refactor.md). Android -device runtime and cross-browser acceptance were not repeated for this change. - -## Previous Flutter Web integration research - -[The integration study](../research/flutter-web-integration.md) confirms that -`sqlite3` already supplies Web engine/storage support and shared database APIs. -A temporary package with Web-only bundled worker/WASM assets passes actual Flutter -3.47.4 release builds: JS, WASM without isolation headers, and WASM with isolation -headers each pass the 20 browser scenarios. Long SQL also allows Flutter animation -ticks to advance. Deep routes under a subpath, page reload recovery and migrations -are included; [captured evidence](../research/validation/flutter-web-probe.json) -records actual compilation mode and asset requests. - -This is a research probe, not a shipping packaging change. The recommendation is -package-owned resources and a simpler SQLite entry, with the platform transport -kept internal. CDN/CSP, cross-browser behavior, multi-tab ownership, hot restart -and offline deployment still need design/verification. The native Flutter example -and current manual Web options remain unchanged. - -## API and correctness refactor - -The comprehensive review of public exports, generation, SQL/decoding, writes, -session ownership and migration boundaries is recorded in -[the audit](../research/api-correctness-audit.md). It adds reproduced regressions -for cross-session subqueries/CTEs, optional-join presence guards, mutation -validation, concurrent shutdown and cancellation notification. Generated row -exports, immutable schema lists, target-specific declaration checks and native -catalog import round-trips are verified. The complete native suite passes -**872 tests in 10:41**, with real SQLite and PostgreSQL 18.4 enabled. Real Chrome -JavaScript and Dart WASM each pass **20 checks**, including the new query boundaries. -Static analysis has no issues. See [final validation](../research/validation/api-audit.md) -and the [API/mental model guide](api.md). - -The full suite includes static generation, build_runner recovery, source/registry -fingerprints, CLI lifecycle, actual process-exit migration recovery and AOT history -without source assets. Temporary consumer tests reuse existing SQLite downloads; -the normal native-asset hook still verifies their hashes. No driver configuration -or consumer packaging path is substituted. - -The previous acceptance records below remain tied to their recorded revisions. -Android/Apple runtime and performance measurements were not repeated in this -review. Work stays on `next`, with local commits only and no push/publication. - -## Previous migration correction - -The single-engine migration correction is implemented. Each registry fixes one -database engine, including before its first migration; files contain one step list -and only the selected physical expressions. Mixed histories/connections are -rejected before migration SQL. PostgreSQL execution checks server 18+, while -SQLite keeps its 3.35+ driver gate and native rebuild behavior. - -The [current validation](../research/validation/migration-targets.md) records 845 -passing full-suite tests and two PostgreSQL crash-test registry configuration -failures, followed by 21 passing target/recovery tests after correcting those -fixtures. These are separate invocations. Source-independent AOT migration and -historical backfill bundles pass; real Chrome JS/WASM each pass 19 checks; the -public registry and example CLI workflows pass; static analysis is clean. -Android was not rerun for this correction. Earlier platform captures below remain -evidence for their recorded revisions. - -The Dart schema/migration workflow is implemented and verified. Current snapshots, -fixed historical migration definitions and static registries are Dart libraries; -the project entrypoint owns typed connection/rename/conversion inputs. The JSON -artifact readers and CLI options are removed without a compatibility lane. Named -SQL generation also emits only Dart and checks freshness against the declaration -and SQL source. Internal checksum/checkpoint encodings and reports remain JSON. - -The earlier [Dart migration acceptance](../research/validation/dart-migrations.json) records -837 passing tests in the full run and one stale test-fixture path, then a passing -76-test focused run covering that correction and the final generation changes. -This is two invocations, not a single all-green full run. The independent AOT bundle -check passes after deleting consumer Dart sources and build cache; an AOT historical -backfill additionally proves bounded pause and concurrent exact-once resume. -At that revision, Chrome JS/WASM each passed 19 checks. Android debug install, release -AOT APK upgrade and distinct-process restart pass 4/17/11 checks with compiled Dart -history and no migration JSON assets. The runnable project migration entrypoint -also passes check/apply/verify/no-replay against a disposable SQLite file. - -The common lifecycle in `research/new-dart-orm-design.md` is implemented and -accepted. The [design acceptance map](acceptance.md) connects each requirement to -evidence; the [capability review](capabilities.md) states extensions and unverified -targets. This is a local implementation, with no package publication or push. - -Earlier baseline: [835 native tests](../research/validation/native.json) pass -in one invocation with PostgreSQL enabled. The [usage capture](../research/validation/usage.json) -passes dependency resolution, generation, the README and teams examples, nine -query cookbook checks on each backend, migration-chain validation and clean -analysis. Real Chrome JS and Dart WASM each pass 19 -scenarios, including temporal column precision and extended date/instant ranges. -An actual Android Flutter application additionally passes 4/17/11 checks across -legacy installation, AOT release APK upgrade and process restart; see -[native Flutter evidence and limits](flutter.md). - -Editor verification additionally covers real generated APIs at 10/100/1000 models: -fifteen completion probes with 300 warm samples, twelve diagnostic checks matching -exact codes/source ranges, and language-symbol rename responses. See the -[captured protocol measurements and limits](generation.md#editor-completion-diagnostics-and-rename). - -The same-driver runtime cost baseline covers SQLite and PostgreSQL, including -controlled TCP latency, concurrency and separate allocation traces. That baseline -uses runtime source `94d24fe`; see [measurements](performance.md). Selection decoding -has since been optimized, with a separate `d0a8c9a` capture confirming fewer -intermediate List allocations; it does not establish a throughput improvement. - -## Completed - -- Reset the `next` branch contents, preserving the design research and license. -- Establish one Dart 3.13 package and local-only commit policy. -- Single-engine migration histories, flat saved steps, selected schema fingerprints, fixed registry targets, connection/version gates and target-specific destructive-change handling. -- Dart physical snapshots, fixed migration files, per-file fingerprints, static registries and typed project CLI configuration; generated output cannot overwrite its source declaration. -- Direct retirement of schema/migration/named-query JSON artifacts, with source-based named-query freshness checks and compiled application migration history. -- Typed scalar expressions, 2–6-field composable projections and dynamic field maps. -- Immutable filters, order, offset/limit, count/exists, basic aggregation and mutations. -- Parameter binding, table occurrence scope checking, explicit default/null changes. -- Native SQLite worker with verified foreign keys, journal mode and parameter limit. -- Browser SQLite worker with explicit memory/OPFS storage, exclusive ownership, verified journaling/foreign keys and shared native codecs/functions. -- Real Chrome acceptance for JavaScript and Dart WASM clients, including persistent reopen, interrupted-transaction recovery and schema upgrades after page reload. -- Actual Android Flutter acceptance with generated APIs, persistent file storage, old debug APK to new AOT release APK upgrade, distinct-process reopen, watch/rollback behavior and background cancellation/reuse. -- PostgreSQL driver with a single pool owner, TLS settings and explicit borrowed pools. -- Transaction lifecycle, rollback, savepoints, pending work checks and query observation. -- Non-executing SQL/column/key/join/batch inspection and optional acquisition/SQL/decode phase observations with immutable metadata and isolated observer failures. -- Reproducible same-driver/SQL/result runtime comparisons with latency samples, concurrent throughput, logical/protocol bytes, lease distributions, live heap and separate selected allocation traces. -- Transaction-wide deadlines/cancellation with native SQL interruption, callback expiry and awaited cleanup. -- SQLite actual transaction-state reporting, automatic rollback recovery and invalidated-savepoint protection. -- Conservative server failure classification distinguishing known commit rejection from unknown outcome. -- Opt-in transaction replay and SQLite COMMIT-only retry with shared attempt/time budgets and cancellable backoff. -- Analyzer-based record generator, scalar annotations, composite keys, indexes and FK validation. -- Named SQL files with generated Record parameters/results, dialect-specific methods, native structure checks and stale-output detection. -- Explicit named-query build_runner assets, CLI generation/checking and shared query/transaction/stream/watch execution. -- Generated table accessors, named create parameters, byId, typed patches and schema snapshots. -- Database computed expressions with stored/virtual modes, typed read-only fields, catalog/import metadata and reviewed migrations. -- Batched and nested relation projections, per-parent SQL window pagination, optional/required relations. -- Runnable generated many-to-many membership example with role/time payloads, composite keys, bidirectional queries, transactional writes and measured query/row counts. -- Explicit `relatesTo` query navigation without a database FK, including nonunique/composite/self/inverse edges and unchanged physical snapshots. -- Correlated relation any/none/every/count; UNKNOWN fails every, and empty sets satisfy it. -- Dialect DDL, cyclic PostgreSQL FK creation, immutable migration checksums and atomic history. -- Concurrent migration serialization, read-only planning, column inspection and column drift checks. -- Runnable generated-client example with migrations, transactions and relationship selection. -- Atomic upsert with declared unique targets and typed existing/incoming field expressions. -- Batch inserts with parameter-aware chunks, optional field shapes, returning and all-chunk rollback. -- Explicit alias joins/self joins, guarded outer-join projections, scalar/IN/EXISTS subqueries. -- SQL window expressions and pre-execution aggregate/grouping placement checks. -- SQL UNION/UNION ALL with typed 2–6-field SQL records, scalar operands and post-query mapping. -- Positional column/codec/nullability validation, scoped branch clauses, CTE exports, streams and set subscriptions. -- PostgreSQL typed standalone parameter projections and exact integer aggregate decoding. -- CTEs with typed references to exported SQL expressions, mapped row decoding and nullable exports. -- Keyset pagination with unique tie breakers, mixed directions, explicit null ordering and versioned cursor transport. -- Actual generated-API negative type tests and checked codec calls after generic type erasure. -- Connection leases spanning transactions, with scoped lifetime and explicit discard. -- Portable schema snapshots; catalog checks for columns, defaults, keys and simple indexes. -- Existing-database baselines verify declared schema facts before recording immutable history. -- Read-only catalog import drafts Record declarations, physical-name maps and review reports, followed by the normal generator/baseline workflow. -- Import preserves supported defaults, identities, composite/unique keys, indexes and named inverse relations; unsupported structures have explicit issues. -- Signed 16/32/64-bit integer column metadata, native PostgreSQL types and verified SQLite range constraints, independent of value/aggregate codecs. -- Width-aware generation, catalog import, immutable snapshots, rename/type migrations, identity sequences and historical backfills. -- Finite exact Decimal values, typed SQL arithmetic, numeric SQLite collation and native PostgreSQL NUMERIC, including keys, relations, aggregates and windows. -- Decimal generation/import, catalog drift checks, reviewed conversion migrations and exact historical backfill keys. -- Declared decimal precision/scale, consistent ORM write coercion, exact SQL precision casts, constrained defaults and width-preserving import/migrations. -- Exact SQL decimal division and six explicit rounding modes, including full finite-range checks and window projections with preserved filtering/grouping/pagination. -- Exact rounded decimal averages with bounded PostgreSQL component sums, incremental SQLite accumulation and shared window inputs. -- LocalDate, LocalTime and LocalDateTime with microsecond precision and full finite native PostgreSQL ranges, independent of DateTime and session timezone. -- Temporal column precision from zero to six digits, matching PostgreSQL rounding in SQLite, with generated writes, explicit value/SQL rounding, default/computed coercion, catalog/import and reviewed precision migrations. -- Calendar generation/import, SQLite indexed collations, normalized relation keys, typed cursors and resumable historical date-key backfills. -- Explicit UTC instant storage, offset-equivalent SQLite indexing, strict process-independent decoding and overflow-checked native PostgreSQL DateTime decoding. -- Reviewed legacy timestamp upgrades preserve captured migration checksums and roll back invalid values or newly equivalent SQLite unique keys. -- Read-only application startup version checks with explicit compatibility ranges, full history validation and recovery-state rejection. -- Unmodeled constraints, expression/partial indexes, triggers and policies are reported separately. -- Reviewable migration diffs, explicit renames/conversions, destructive-change gates and checksum chains. -- SQLite atomic copy/rebuild with preserved dependency SQL, foreign-key validation and state restoration. -- PostgreSQL constraint changes resolve actual catalog names, including baselined schemas. -- Migration CLI: create/check/plan/apply/status, table inspection, schema verification and baselines. -- Explicit SQLite read-only connections; CLI read operations never create a missing database. -- PostgreSQL recoverable autocommit steps with boolean pre/postconditions and immutable attempt checksums. -- Durable per-step progress, exact concurrent-index state checks, explicit INVALID-index repair and CLI visibility. -- Historical-schema backfills on both databases with bounded primary-key batches, atomic data/cursor commits and completion proofs. -- Shared batch budgets, lossless composite cursors, competing-runner recovery and committed subscription notifications. -- Shared migration/baseline session locks use bounded try-lock polling without retaining waiting snapshots. -- Database cursors with demand-driven batches, per-batch relation loading and scoped stream cleanup. -- Real SQLite native interruption and PostgreSQL control-connection cancellation with awaited cleanup. -- Cancellable, bounded connection acquisition with monotonic entry checks and safe late-lease draining. -- Independent PostgreSQL pool/connection deadlines, with acquisition settings propagated through queries, mutations, batches, streams and watches. -- Per-statement deadlines, poisoned-transaction protection and atomic cancellation between batch chunks. -- Explicit cancellation capabilities, borrowed-pool ownership and failed-control connection disposal. -- To-one JOIN strategy using declared uniqueness, with explicit join/batch choices and cardinality checks. -- Composite-key batch execution, tuple matching, parameter accounting and nested JOIN/window combinations. -- Joined optional/required projections preserve row presence, nullable values, CTE exports and cursor decoding. -- Constant schema codecs retain custom classes, extension IDs, nullable aliases and nested generic/record types. -- Portable enum text labels, generation-time codec matching and stable imports for separately declared domain types. -- JSON scalar decoding and explicit SQL-null/document-null distinction across SQLite and PostgreSQL. -- Typed query subscriptions with commit/savepoint boundaries, relevant-table filtering and declared FK delete effects. -- Dependency discovery across joins, subqueries, CTEs and batch relations; explicit raw/external-write notifications. -- Paused subscription coalescing, stale-read suppression, active-query cancellation and driver-wide subscription cleanup. -- Opt-in build_runner integration with explicit roots, shared generation/formatting and resolver-owned dependency tracking. -- Asset-based and real-process generation checks, imported metadata invalidation, upstream builder inputs and stale-output cleanup. - -## Delivery sequence - -1. Typed schema metadata, SQL expressions, projections, query and mutation execution. -2. Native SQLite worker and PostgreSQL drivers, explicit transactions and savepoints. -3. Record schema generator, typed table entry points and relationship declarations. -4. Batched relationship projections, root and per-parent pagination, nested relations. -5. Migration snapshots, dialect DDL, history/checksums, introspection and drift checks. -6. Advanced SQL, custom codecs, streaming, observation, Web adapter and capabilities. -7. CLI, examples, user documentation, real-database and platform acceptance checks. - -## Validation - -The following paragraphs are the chronological implementation record. References -to pending work describe that stage; the current acceptance decision and remaining -extension boundaries are in the final review below. - -The research type proof was analyzed and ran with JIT and AOT; JavaScript compilation -also passed. These checks do not constitute a working ORM or browser validation. - -Static analysis is clean. The complete suite passes 740 checks with native SQLite -and a disposable PostgreSQL 18.4 instance enabled. It exercises generation, -composite-key source validation, projections, relations, per-parent pagination, -transactions, migration rollback/history and the generated application client. -This includes a SQLite catalog regression for nullable non-rowid primary keys -and three independent CLI process workflows (SQLite lifecycle, SQLite baseline, -and PostgreSQL lifecycle, including autocommit plans/progress). The full suite -passes in one invocation. Thirteen recovery checks include two real process exits -at durable SQL/commit boundaries, INVALID indexes, lock deadlines and concurrent -runners; committed work is not replayed on resume. -`dart run example/main.dart` runs successfully against a real in-memory SQLite DB. -The generated SQLite example also compiles and runs as a native macOS AOT executable. -Thirty-seven streaming/execution checks cover native cursor fetches, pause/resume, -early exit, relation loading, deadlines, cancelled writes, failed control connections -and delayed cancellation races. A batch-cancellation regression was first reproduced -as a partial commit and now verifies rollback on both databases. -`test/support/native_execution.dart` additionally compiles and runs as a macOS AOT -executable, verifying joined records, nullable required values, absent relations, -cursor consumption, `sqlite3_interrupt`, subsequent SQL and committed query subscriptions. -The relationship fixture is generated from a record schema with nullable composite -foreign keys and self-relations. It verifies 1200-key batches, key deduplication, -per-parent windows, nested JOINs, whole-row absence and root pagination on both -backends. Generator checks compare committed client code and schema snapshots. -The domain-type fixture adds 18 real-database checks for extension IDs, custom -classes, enums, structured JSON, nullable document presence, relations, batches, -upsert, cursors and schema catalogs. JSON string scalars are decoded without a -second parse; SQL NULL and JSON null are separately represented, and required JSON -documents reject SQL NULL. Generator checks cover moved outputs, colliding domain names, nullable -aliases and invalid annotations. `test/support/codecs/native.dart` passes as a -macOS AOT program with domain values, relations and streamed cursors. -Forty-one subscription checks cover both backends, including independent SQLite -workers and PostgreSQL connections, transactional notification merging, savepoint -rollback, zero-row writes, multi-level cascade/SET NULL effects, joined CTEs and -batch relations. They also verify paused-listener cleanup, native cancellation, -read/invalidation races and re-reading after a real PostgreSQL commit whose -acknowledgement is deliberately lost. Subscriptions add no browser/Flutter -acceptance claim. -Eight asset-builder checks and one real consumer-process workflow verify the -standard build_runner path, prior-builder inputs, imported metadata invalidation, -error recovery, explicit root selection and deletion/recreation of generated -outputs. CLI and builder code/snapshots match. The generator formats in-process. -The standalone generation benchmark records 10/100/1000-model first builds, -unchanged builds, watch edits, static analysis and output sizes with pinned -environment metadata. On the recorded Apple M3 Max run, single-field watch edits -took 0.52/0.65/2.17 seconds. These are single observations with warm SDK/pub caches, -not latency percentiles, IDE completion measurements or runtime SQL benchmarks. - -Thirty-two set-query checks cover both backends, including repeated positional -columns, nested set association, operand-local CTE names/order/limits, final DTO -mapping, outer-join nullability, shared domain codecs, all built-in constant types, -SQL NULL versus JSON null, scalar subqueries, native streams and committed changes -from either operand. Negative compilation includes mismatched scalar and Record -operand types. The native macOS AOT integration also verifies typed UNION Record -streaming. Whole generated entity/Dart-mapped projections must explicitly select -SQL `.row` fields before UNION; arbitrary mapper equivalence is never inferred. - -Connection acquisition is verified with twenty shared SQLite/PostgreSQL checks, -three additional PostgreSQL checks (initialization, connection versus queue limits, -and native pool timeout classification), and a controlled event-loop starvation -regression using the native SQLite driver. Timed-out mutations, batch transactions, -sessions and queries never execute after their late leases arrive. Stream -cancellation, watch recovery, pool PID reuse, resource draining and application -error propagation are checked. macOS AOT also verifies that an expired queued -write never runs after the held lease is released. Acquisition controls remain distinct from transaction-wide execution deadlines. - -Forty-one transaction-control checks cover SQLite and PostgreSQL plus actual -concurrent connections and failure-injecting driver wrappers. They verify idle -and CPU-bound callbacks, active reads/writes/cursor fetches, paused streams, -borrowed sessions, savepoint cleanup, expired connection acquisition, native -BEGIN/COMMIT lock waits, automatic rollback, deferred constraint rejection, -known COMMIT acknowledgement races, lost acknowledgements and failed rollback. -PostgreSQL serialization failures and deadlocks are produced by real competing -transactions. SQLSTATE 40003 at COMMIT remains conservatively unknown. Native -macOS AOT verifies transaction expiry and automatic rollback across a savepoint. -Twenty-nine retry checks verify explicit opt-in, exact attempt limits, acquisition -and execution time budgets, cancelled backoff, fresh transaction views, discarded -failed-attempt notifications, unknown commits and failed rollback. Actual competing -PostgreSQL connections produce serialization/deadlock conflicts; SQLite workers -produce WAL busy-snapshot and DELETE-journal COMMIT conflicts. Both callback replay -and COMMIT-only retry share one finite budget. The acquisition timer preserves its -own error class rather than inferring the source from an elapsed clock. -`test/support/native_retry.dart` compiles and runs as a macOS AOT executable against -two SQLite workers, verifying snapshot replay and COMMIT retry without replaying -the successful callback. - -Nineteen application-version checks cover both databases: exact latest and explicit -inclusive ranges, unversioned databases, rejected downgrades, old checksum changes, -missing history, baselines, separate catalog drift, borrowed sessions and invalid -ranges. PostgreSQL checks use a real partially completed recoverable migration, -repair/resume and concurrent checkpoint creation during a consistent read snapshot. -The caller's history list is frozen before asynchronous reads. A read-only SQLite -file accepts a compatible version without applying migrations. The native macOS -AOT execution fixture also checks accepted and rejected schema versions. - -Fifty-six backfill checks cover both databases: bounded pause/resume, unchanged -historical JSON/checksums, atomic cursor/data rollback, failed completion proofs, -compatible and incompatible concurrent writes, competing runners and lost COMMIT -acknowledgements. Four actual child-process exits at data-write and commit boundaries -verify that committed chunks are retained and uncommitted chunks are repeated only -after rollback. Native integer, bigint, text, finite real, boolean, timestamp and -binary keys survive resume, including composite keys under a reduced parameter -limit. Additional checks cover ordinary migration grouping, SQLite rebuild/FK -restoration, old checkpoint-table upgrades, invalid cursors, temporary-object -shadowing, PostgreSQL row security and inherited duplicate keys. Independent CLI -processes inspect plans, pause, report progress and finish backfills on both -databases. Committed chunks notify typed subscriptions, and completion probes -reject floating-point truthiness. `test/support/native_backfill.dart` compiles and -runs as a macOS AOT executable, verifying historical declarations, bounded runs, -concurrent SQLite workers and the application startup version gate. The existing -native execution acceptance fixture also passes after this change. These are -correctness checks, not backfill throughput or online availability measurements. - -Seventeen catalog-import checks verify both databases, including physical/Dart -name collisions and escaped identifiers, identities/defaults, composite/self -relations, unique indexes, read-only SQLite, scoped sessions, excluded unsupported -types, nullable primary keys, virtual/shadow tables, -PostgreSQL inheritance and specialized index semantics. Draft declarations pass -the real analyzer/generator and produce matching schema snapshots. An imported -client runs against PostgreSQL, and the SQLite client compiles and runs as a macOS -AOT executable; both preserve existing rows and execute typed relationship queries, -defaulted writes and SET DEFAULT deletes after a verified baseline. Two additional -CLI workflows independently import, generate, baseline and verify real databases, -reject existing output files, preserve missing SQLite files and retain blocking -issues in review reports. The generator no longer emits invalid empty patch -parameters for models containing only generated fields. Physical import currently -uses the exact supported storage mappings documented in `doc/importing.md`; -Unconstrained NUMERIC now imports as finite Decimal; it is never guessed to contain -only integers. Broader native types remain part of the active goal. - -Twenty integer-width checks cover native signed boundaries, nullable/defaulted -columns, exact native 64-bit values, wider aggregate results, mixed-width UNIONs, -relations, transported cursors, generated/imported snapshots and historical -backfills. Reviewed widening/narrowing migrations run on both databases; overflowing -existing rows roll back structure and history together. Physical renames and -identity-width changes preserve generation, including the PostgreSQL sequence -type. SQLite checks distinguish emitted range constraints from quoted defaults, -comments and weaker expressions. Analyzer tests reject invalid or repeated widths -and retain integer-backed domain codec types. The new generated fixture is checked -for deterministic source and snapshot output. `test/support/integers/native.dart` -compiles and runs as a macOS AOT executable, verifying boundaries, wider sums, -relations, overflow rejection and catalog checks. No browser numeric claim is added. - -Twenty-nine exact-decimal checks cover canonical values, scientific input, -finite range limits, signed rounding and independent integer arithmetic, plus -both databases' typed writes/defaults/patches, comparisons, IN, sorting, distinct -values, transported cursors, computed CTEs, UNIONs and streaming. Exact sums, -nullable aggregates, grouping and windows are verified, including a SQLite moving -frame and intermediate overflow that cancels before a valid final result. A -numeric key lookup retains SQLite index use; this is a query-plan check, not a -throughput benchmark. Equivalent numeric spellings match joined/batched relations -and reject duplicate unique keys. Catalog import/generation, quoted and Unicode -column collations, default equivalence, historical backfill keys and conversion -migration rollback run against actual databases. Unsupported drivers reject typed -decimal SQL before execution. `test/support/decimals/native.dart` compiles and runs -as a macOS AOT executable, verifying exact values, sorting, aggregate/window -functions, normalized relation keys and catalog checks. Invalid external SQLite -text and PostgreSQL special numeric values fail typed decoding; no finite-decimal -CHECK constraint or cross-client SQLite function installation is claimed. Browser -acceptance remains open; constrained columns, SQL division/rounding and averages -are covered by the additional acceptance checks below. - -Nineteen precision checks cover signed ties, negative and excess scales, maximum -precision/scale boundaries, nullable values, defaults, expression writes, wider -aggregates, SQL casts, CTEs, batches and upserts. Rounded keys participate in real -foreign-key relationships. Catalogs, snapshots, generated/imported declarations, -renames, default changes and historical backfills retain column constraints. -Scale reduction with duplicate rounded keys rolls back structure and history; -repairing data allows the same migration to resume. SQLite rejects raw values -that do not fit, detects missing default coercion and retains an explicitly -disabled trusted_schema setting. The current SQLite binding requires -trusted_schema ON to evaluate the managed precision CHECK/default functions. -Generator checks reject invalid/repeated annotations and preserve decimal-backed -domain types. The precision fixture compiles and runs as a macOS AOT executable. -Exact SQL averages are covered by the additional acceptance checks below. - -Twenty-three division checks compare all signed rounding modes and positive/negative -scales with exact integer arithmetic. They cover 16383 fractional digits, values -around rounding ties, nulls, zero divisors, finite-range overflow and enormous -remainders whose naive intermediate products overflow PostgreSQL NUMERIC. Exact -defaults reject lost digits. Real queries combine division/rounding with grouped -aggregates, windows, HAVING, DISTINCT, ordering, pagination, outer joins, CTEs, -UNIONs, correlated subqueries and per-parent batched relation pagination. Window -expressions retain frame/partition/order identity when referenced through CTEs. -Volatile PostgreSQL operands are evaluated once each; failed expression writes -roll back their transaction. The updated decimal fixture compiles and runs as a -macOS AOT executable, including exact SQL/window division and configurable rounding. -PostgreSQL uses scalar quotient/remainder stages and an additional projection for -window operands; native arithmetic throughput is not implied. -The reproducible decimal benchmark separately records native AOT 10000-row -read/divide/round/window queries on SQLite 3.51.0 and PostgreSQL 18.4. Its raw -report is `research/benchmarks/decimal-division.json`; methodology and medians -are in `doc/decimals.md`. This is a small single-client end-to-end cost probe, -not the complete runtime performance acceptance or a high-precision stress benchmark. - -Twenty-three average checks cover exact integer fractions, empty/all-null/constant -inputs, signed ties and every rounding mode, maximum fractional scale and explicit -final-result overflow. Means remain valid when an intermediate total exceeds the -finite NUMERIC range, including cancellation between extreme values. Real queries -cover grouping, HAVING, unique-input CTEs, UNIONs, streaming, partitions, mixed -windows, DISTINCT results, pagination and windows over grouped sums. Correlated -averages update fields; failed exact rounding rolls back the containing transaction. -Per-parent relation windows retain limits. PostgreSQL sequence tests prove one -evaluation per contributing input/order row, including repeated source values; -SQLite moving-frame tests remove values, reset empty frames and reject varying -rounding policies. Illegal aggregate assignments and nested windows fail before -execution. The updated decimal AOT fixture verifies ordinary and running averages. -The expanded native benchmark records 10000-input-row averages and running -averages in `research/benchmarks/decimal-average.json`, together with read/divide/ -round controls. It uses the same bounded single-client method and records returned -row counts separately; it does not establish concurrency or large-coefficient cost. - -Twenty-four local temporal checks verify Gregorian era/leap boundaries, random -full-range ordinal round trips, finite PostgreSQL endpoints, microseconds and -24:00. Real SQLite/PostgreSQL checks cover generated writes, defaults and null -patches, ordering/min/max/windows/grouping, CTEs, UNION, streams, equivalent -relation keys, unique indexes, cursor transport, imported declarations and -historical date-key backfill recovery. Reviewed text-to-time conversion rejects -equivalent unique keys and rolls back. A reproduced PostgreSQL default comparison -regression now retains expression casts that discard time. Invalid external dates -and infinities fail typed decoding without damaging the connection. Borrowed -PostgreSQL pools explicitly opt into the configured calendar registry; the -unconfigured path rejects temporal queries before execution. A second negative -compilation check rejects five calendar/instant/string mismatches. - -`test/support/temporals/native.dart` compiles and runs as a macOS AOT executable, -covering endpoints, calendar order, bounded streams, equivalent relation keys and -catalog/default verification. These checks do not establish browser behavior or -temporal query throughput. Column precision, calendar SQL arithmetic and timezone -conversion are still pending. The local-type checks are distinct from the UTC -instant corrections verified below. - -Twenty UTC instant checks reproduce and correct the prior SQLite microsecond -ordering and process-timezone decoding failures. Native SQLite/PostgreSQL tests -cover common DateTime endpoints, microsecond round trips, offset-equivalent -predicates/unique keys/relations, indexed lookups, grouping/windows/CTEs/UNION, -streaming, cursors, imported DateTime declarations, literal defaults and expression -cast drift. Instant-key backfills resume across BC and sub-millisecond boundaries. -Out-of-DateTime PostgreSQL values retain raw text and fail typed decoding without -integer overflow or connection damage. - -The captured `test/support/instants/m0001_legacy.dart` was emitted using commit -`00853b4`. Its original checksum is asserted after decoding with the current -migration reader. Both databases upgrade it through a reviewed timestamp-to-instant -change; SQLite tests prove rollback on equivalent keys and invalid calendar text. -The new instant storage tag leaves old snapshot/DDL meaning unchanged. Native -drivers use one `temporal` capability for local calendar values and UTC instants; -borrowed PostgreSQL pools require the configured registry before opting in. - -`test/support/instants/native.dart` passes JIT in a Shanghai-timezone subprocess -and as a compiled macOS AOT executable under UTC, Asia/Shanghai and America/New_York. -It checks UTC defaults, local DateTime input conversion, microsecond sorting and -relations. These checks do not prove browser precision, timezone-name resolution, -column precision policies or temporal throughput. - -Twenty-four named SQL checks cover deterministic generation, bound/repeated/nullable -parameters, injection strings, SQL quotes/comments, typed projections, CTEs, UNION, -scalar subqueries, Decimal/calendar/instant codecs, streaming, transaction rollback, -explicit watch dependencies, and native structure failures. Seven additional -invalid API uses fail Dart analysis, including wrong result types and calling a -PostgreSQL-only query on SQLite. The CLI refuses stale generated files and opens -SQLite checks read-only without creating missing database files. - -SQLite EXPLAIN and PostgreSQL PREPARE checks do not execute the application SELECT; -throwing expressions and an unchanged PostgreSQL sequence verify that boundary. -PostgreSQL prepared statements are deallocated after storage checks. Result -nullability, value ranges and custom codec semantics remain explicit declarations -with runtime decoding, rather than claimed database inference. - -The real build_runner build/watch process also tracks named SQL assets and imported -enum changes, recovers from SQL parameter errors, and removes both query outputs -when their source is deleted. The native named-query fixture compiles and runs as -a macOS AOT executable with SQLite. See -`doc/named-sql.md` for the API and validation limits. - -Sixteen browser acceptance scenarios pass in Chrome 153 with a JavaScript client, -and the same sixteen pass with a Dart WASM client. Both use a separately compiled -JavaScript database worker and the pinned sqlite3 3.6.0 WASM asset. They cover -memory migrations/catalogs, FK enforcement, generated records and relations, -rollback/savepoints/transaction lifetime, bounded cursors, watch snapshots, exact -BigInt/Decimal/blob/calendar transport, unsupported interruption, responsive UI -events during a long query, bounded startup failures and exclusive OPFS ownership. -Committed rows survive closing/reopening and an actual page reload while a second -transaction is still open. Reload recovery discards that transaction, passes -integrity_check, and applies the next schema version without losing prior rows. - -Browser acceptance reproduced two numeric differences: integral doubles can be -mistaken for unsafe integer parameters, and epoch-microsecond arithmetic loses -precision for remote DateTime values. Explicit SqlReal binding and component-based -instant decoding preserve their intent and precision in both compilation modes. -Floating keyset cursor tokens preserve their value and stable tie breaker. -Checks include BC microseconds and DateTime's upper boundary. Two additional real -native database checks verify SqlReal floating storage on SQLite and PostgreSQL. -The native UTC instant AOT fixture is revalidated after sharing SQLite functions -with the web worker. Captured browser results are in -`research/validation/browser.json`; setup and limitations are in -`doc/sqlite-web.md`. These correctness checks do not measure throughput or certify -Safari, Firefox, Flutter embedding, IndexedDB or a shared multi-tab service. -Statement interruption remains explicitly unsupported by this web driver. -After the floating-cursor correction, all 54 native query/floating-parameter -checks pass again, as do both complete browser runs and static analysis. - -The native full suite passes in one invocation with PostgreSQL enabled. macOS runs -test files serially because separate Dart CLI startups can concurrently rewrite -and codesign the shared SQLite native-asset cache before application code starts. -Explicit concurrent database operations inside tests remain enabled. Browser -acceptance commands run separately from the native suite. - -Twenty-four unconstrained-relation checks cover real SQLite/PostgreSQL catalogs, -missing/nullable targets, nonunique cardinality, per-parent pagination, self and -inverse navigation, correlated predicates, streaming, transactions and watch -dependencies. The declaration adds no FK, index, uniqueness promise or cascade. -Adding a real FK over dangling data fails and rolls back; repairing rows permits -the same reviewed migration to proceed. Removing it preserves rows and permits -unmatched references again. - -Seven generator checks distinguish query navigation from physical constraints -and reject invalid selectors, types, codecs, names and referential actions. -Five additional invalid generated API calls fail static analysis. Existing FK -fixtures remain byte-for-byte deterministic. Browser JS and Dart WASM acceptance -also covers generated query-only relations with single and composite REAL keys. -This reproduced a missing floating binding tag in relationship batches; grouping -now retains comparable storage values and binding restores SQL floating intent. -See `doc/relations.md` for declaration and integrity semantics. -The complete native suite passes all 703 checks in one invocation after these -changes, with PostgreSQL enabled. Both complete browser runs and static analysis -also pass. The design acceptance map records the remaining product gates. - -Seventeen CHECK checks cover generated named/unnamed constraints, SQL NULL, -native write failures, import and baseline without replacing rows, duplicate -constraints, drift, casts/precedence, missing/type-changed columns and SQLite -Unicode identifiers. Add/change/remove migrations validate old data and roll back -history on failure; explicit renames preserve incoming references, indexes, -views and SQLite triggers. Unclaimed checks still block SQLite rebuilds. -PostgreSQL unvalidated, NO INHERIT and NOT ENFORCED constraints stay unmanaged; -verification plans but does not execute volatile row expressions. - -The generator additionally rejects dynamic/empty CHECK SQL, empty names and -duplicate names. CLI inspection exposes names and expressions. Older snapshots -without checks preserve their JSON/checksum representation. CHECK SQL is trusted, -uses physical names and may have backend overrides; PostgreSQL comparison has -planning cost, and checked SQLite renames may copy a table twice. These boundaries -are documented in `doc/checks.md`. - -The complete native suite passes 721 checks in one invocation, with PostgreSQL -enabled, after updating an old integer-test assertion for the newly recognized -CHECK catalog category. Static analysis reports no issues. Real Chrome JS and -Dart WASM runs each pass 15 scenarios, including generated CHECK enforcement, -atomic constraint migration failure/retry and persistent upgrades. The browser -report records both runs; these are correctness checks, not performance results. - -Seventeen client-default checks cover real SQLite/PostgreSQL creation, typed -domain IDs, nullable values, DateTime constructors, optional static-method -arguments, generic factories and const function references. Explicit values/null -and database DEFAULT bypass the corresponding factory. Prepared inserts and -batches call each omitted factory once and retain values through compilation, -execution and conflict clauses. Updates do not apply insert defaults; database -rollback does not undo Dart side effects. Explicit DEFAULT on a client-defaulted -identity retains each backend's native sequence behavior. - -Generation and moved-output analysis do not execute application factories. -Generator negatives reject incompatible/async scalar results, erased domain -types, required arguments, private/nonconstant references and repeated annotations. -Four invalid generated create calls fail static analysis. Snapshots and imports -contain no client factories; physical defaults and migration checksums remain -separate. Tables without client factories take a direct insertion path, while -other tables cache their default-bearing columns. See `doc/defaults.md`. - -The complete native suite passes 740 checks in one invocation with PostgreSQL -enabled, and static analysis reports no issues. Both real Chrome compilation -modes pass 16 scenarios, adding client omission versus explicit null and prepared -batch behavior. The browser schema snapshot itself is unchanged by adding a -client factory. No throughput measurement is claimed by these correctness runs. - -Nineteen computed-column checks cover SQLite and PostgreSQL stored/virtual CRUD, -nullable results, batch/conflict returning, generated relationship keys, quoted SQL, -catalog/import roundtrips and drift. Existing rows survive add/drop, expression and -result-type changes, stored-to-ordinary materialization, renamed CHECK expressions, -indexes, incoming references and views. Invalid recomputation rolls back rows, -schema and migration history. Decimal computation retains precision casts through -import and generation. Ten invalid declaration cases and seven invalid generated -API uses fail source/type checks; low-level copy/backfill maps reject computed -targets. CLI inspection returns computation SQL and storage mode. - -The implementation uses `ReadField` for computed results and excludes those -fields from generated creation/patch inputs. Native PostgreSQL mode/dependency -limits remain explicit; automatic diff requires reviewed replacement plans for -mode conversions it cannot emit natively. Renamed computed SQLite tables may -require two copies. See `doc/computed.md` for these costs and migration limits. -The complete native suite passes 761 checks in one invocation with PostgreSQL -enabled, and static analysis reports no issues. Real Chrome JS and Dart WASM each -pass 17 scenarios, including computed updates and migration recomputation alongside -the existing OPFS recovery/upgrade scenarios. The captured browser report records -both runs; these do not establish throughput or native Flutter behavior. - -Eighteen many-to-many checks exercise the generated `example/teams` schema against -SQLite and PostgreSQL. Memberships carry an enum role and UTC joining time, with -two foreign keys and a composite primary key. Forward/reverse pagination returns -only requested associations; root pagination and empty results remain independent. -Nested team rosters deduplicate shared keys, parameter limits trigger bounded -chunks, and correlated predicates/counts remain in one statement. Tests assert -actual statement counts and fetched-row volume alongside payloads and endpoint -identity. Transactions roll back partial endpoint/association writes; composite-key -conflicts preserve joining times, unlinking preserves endpoints, cascades remove -only affected memberships, and watches track committed payload/endpoint changes. - -Six invalid generated API uses fail analysis, covering composite keys, enum -payloads, result shapes and nonexistent implicit graph writes. The new example is -part of deterministic generation checks and runs directly, reporting two SQL -statements returning one root and two association rows. The targeted native -invocation passes 54 checks (many-to-many, generator and types), and static analysis -reports no issues. This stage adds examples and acceptance coverage over the -existing runtime; the last full-suite run remains the 761-check invocation above. -Real Chrome JS and Dart WASM each pass 18 scenarios. Both exercise the same teams -schema, checking association payloads, per-parent limits, two-statement reads, -transaction updates and endpoint cascades. The browser report retains both runs. - -Twenty-seven new plan/observation checks pass against real SQLite and PostgreSQL. -Inspection uses existing statement compilers, without acquiring a connection, -decoding fabricated rows or invoking result mappers. It exposes root and one-key -child SQL templates, projection/presence/key slots, joins, read dependencies, -per-parent pagination and composite-key chunk capacity. Template count is distinct -from data-dependent execution count. Bound values are omitted; literal SQL text -is retained. Raw dependencies stay explicitly opaque. - -Optional acquisition and decoding hooks propagate into sessions, transactions -and savepoints. Tests cover real pool/lease waiting, timeout/cancellation and -late-lease draining, SQL versus mapper failures, nested row grouping, cursor -batches and returning writes. Observer exceptions preserve commits and original -errors. Hooks omitted means no observation events or measurement Stopwatches; -inspection is only constructed when requested. These phase measurements exclude -some client work and do not isolate server CPU or provide trace correlation IDs. -See `doc/observability.md` for the exact scope and runnable example. - -Static analysis passes. The updated teams example executes two SQL statements, -returns one root/two child rows and reports one acquisition/two decode batches. -The full native invocation passes 807 checks with PostgreSQL enabled. Real Chrome -JS and Dart WASM each pass 18 scenarios, including non-executing plan inspection, -one acquisition and child/root decode observations for generated memberships, -alongside OPFS persistence, reload recovery and upgrades. The captured browser -report contains both runs. This establishes behavior, not throughput or allocation -cost; native Flutter and the remaining acceptance gates are still open. - -The runtime cost harness completes four workloads on SQLite WAL, local PostgreSQL -and a delayed TCP relay, retaining 200 raw/ORM samples for both paired latency and -eight-client throughput. Probes verify exact SQL/parameters, result shapes and row -volumes; PostgreSQL protocol bytes and SQLite logical JSON byte counts have distinct -labels. Independent echo calibration measured 26.410 ms p50 for the nominal 20 ms -delay. This is a local controlled-latency experiment, not a remote-server deployment. - -Separate processes measure live isolate-group heap/RSS and selected main-isolate -allocation traces. The SDK's current `accumulatedSize` implementation reports live -size, so the harness does not misrepresent it as total allocation. Timing processes -have hooks, VM service and profiler disabled. Captured tool hashes, sample counts, -SQL/parameter equality and result volumes were independently read back. Analysis -reports no issues; no runtime source changed in this stage, so the 807-test and -JS/WASM correctness runs above remain the runtime baseline. - -The results identify intermediate List construction and PostgreSQL protocol -round trips as concrete optimization candidates. There is no new performance -percentage guarantee or claim of superiority over another ORM; detailed numbers, -scope and reproduction commands are in `doc/performance.md`. - -Selection decoding now binds field readers once and passes their results directly -to each typed mapper. Dynamic field maps also avoid an intermediate value list. -Related rows are expanded into fixed-length buffers with one copy, retaining -separate storage from driver rows. Public API, SQL, result shapes and mapping order -remain the existing contract. Six new backend checks cover all two-to-six-field -arities, repeated columns, deferred/ordered evaluation, streaming, first-error -propagation and dynamic field snapshots/nulls. The focused selection, relation, -observation, inspection and UNION invocation passes 93 checks. The full native -invocation passes 813 checks with PostgreSQL enabled, and analysis reports no -issues. Real Chrome JS and Dart WASM each pass 18 scenarios; the teams scenario -additionally checks a six-field mixed-type projection and deferred/ordered mapping. -The browser report retains both runs. The separate `runtime-selection.json` cost -capture preserves the original baseline, retaining 200 timing/concurrency samples -per lane and 64 acquisition samples. All twelve scenario/case combinations retain -identical SQL, parameters, row volumes and logical/protocol bytes. Three SQLite -relationship reads reduce selected List/backing-List traces from 8,244 to 5,220; -local PostgreSQL reduces them from 17,564 to 14,541. Normal latency and throughput -do not establish a speed improvement: some timings increased alongside raw-driver -controls, and relay calibration changed between runs. The report records those -limits rather than attributing timing changes to decoding. - -PostgreSQL protocol lifecycle review confirms separate awaited prepare/dispose -work and pending portal cleanup in the pinned driver's Statement implementation. -Caching statements while retaining the current bind path cannot just omit -disposal; cancellation, portal release and connection reuse must stay valid. -The current adapter deliberately retains its verified cleanup behavior. The -measured round-trip cost and scope are documented in `doc/performance.md`. - -Temporal precision now spans declarations, generated/manual columns, immutable -snapshots, SQL assignments and value/expression operations. Default and explicit -six-digit declarations retain the prior canonical snapshot and migration checksum. -SQLite uses managed coercion/functions and CHECKs; PostgreSQL uses native type -modifiers. Runtime rounding uses day-local microseconds to preserve extended dates -on JS without overflowing a total timestamp count. Tests cover all seven precisions -around the PostgreSQL epoch, BC dates, extended timestamp/instant limits, nullable -values, defaults/computed columns, CTE/grouping/streams, batch/upsert and rounded -relation keys. Import roundtrips preserve metadata and avoid duplicate wrappers. -Reviewed narrowing and rename can detect merged unique keys, rolling back schema, -data and history before a successful retry. - -The new 21-check suite and the 17-check catalog import suite pass together. The -full native invocation then passes 834 checks with PostgreSQL enabled. Static -analysis passes. Real JS and WASM each pass 19 browser scenarios, including the -new precision worker scenario and existing persistence/reload upgrades. The -captured browser report includes both runs; no native Flutter claim is added. - -## Editor protocol capture - -`tool/benchmark_editor.dart` launches the installed Dart 3.13.3 Analysis Server in -isolated consumer packages against runtime `7eaab3b`. The checked report is -`research/benchmarks/editor.json`; it retains harness hashes, every warm sample, -startup/readiness costs, diagnostic ranges and applied rename edits. Independent -readback checks all hashes, nearest-rank quantiles, current error ranges and rename -edit counts. Final consumer analysis passes at all three scales. Root static -analysis also passes; runtime sources are unchanged from the 834-native-check and -19-per-browser-scenario capture above. - -At 1000 models, warm table-entry completion is 11.572 / 17.094 ms p50 / p95; -query-field completion is 0.780 / 0.850 ms. Initial project readiness is 3.405 s, -and the query-field probe has a separate 128.488 ms opening/diagnostic wait. These -are stdio protocol observations with twenty warm samples per probe, not complete -GUI latency or a declaration-frontend comparison. - -Record field rename returns null. Primary-constructor and table fields update -their declaration and typed reference; typedef rename updates the alias and its -two references. Rename probes run in a separate clean server: same-session class -field rename after diagnostic recovery stalled during harness development and -is not proven reliable here. A valid Dart arithmetic index selector passes CLI -analysis but the generator rejects it with a source offset, demonstrating the -boundary between Dart and ORM-specific rules. This closes the measured LSP slice, -not the full three-authoring-frontend experiment or whole-product goal. - -## Declaration-form comparison - -`tool/compare_authoring.dart` now executes the design's User/Post/Profile/Follow -experiment using actual Record, primary-constructor and typed table declarations. -Experimental adapters resolve each input, preserve field metadata and map it to a -common Record schema before using the unchanged production generator. The base, -field-type edit and physical-name-preserving field rename each produce identical -canonical source, generated client and physical snapshot across the three forms. -Four composite FKs, compound keys/uniqueness, identity/default/nullability, index -order and CHECK contents are verified independently from the report. - -All forms compile the same generated create/patch and nested relation consumer. -The six stale-consumer checks fail after type edits/renames and pass after their -references/types are repaired. Fourteen negative cases locate Dart, generator or -adapter errors in original source; constructor logic and custom table codecs are -rejected instead of silently lost. Class/table schema rename updates the field and -typed constraint reference through real LSP edits. Record field rename is manual. - -The captured model declarations occupy 487 / 585 / 1,574 bytes and 24 / 24 / 43 -formatted lines respectively. These are fixture observations, not a universal -Record superiority claim. Stage timings have fixed-order JIT/cache bias and are -not a frontend speed comparison. The [report and interpretation](authoring.md) -preserve the distinction between schema-only adapters and nominal runtime rows. -Root static analysis passes. Runtime sources remain unchanged; this work does not -claim additional database or native Flutter validation. - -## Native Flutter APK upgrade acceptance - -The Android application and host runner are now captured in -`research/validation/flutter.json`. Flutter 3.47.4/Dart 3.13.3 loads SQLite 3.53.4 -on an isolated API 35 arm64 emulator. Version code 1 writes two records; version -code 2 is installed with `adb install -r`, adds the new field/relationship, retains -the original physical column and verifies existing values down to microseconds. -The release application checks generated writes/projections, transaction rollback, -commit-driven watch delivery, native cancellation and subsequent worker reuse. -An independently restarted release process verifies three persisted rows and the -comment, unchanged migration checksums, no migration replay and read-only reopen. - -The three phases pass 4/17/11 assertions. A two-million-row recursive sum completes -while main-isolate timer and Flutter animation callbacks advance (15 and 7 in the -final capture). These are progress observations, not an FPS or throughput claim. -Source/APK/screenshot hashes and version/history/row continuity were independently -read back. The stable screenshot was visually inspected. Initial oversized log -chunks were truncated by Android; bounded chunks and a fresh complete run resolved -the report transport issue. ORM runtime source is unchanged. Root analysis and -the bundled migration chain check pass; the prior 834-test native run and 19+19 -browser scenarios remain separate runtime evidence. - -This closes the Android native Flutter gate, not physical-device, Apple-platform -or power-loss acceptance. Reproduction and exact scope are in [Flutter](flutter.md). - -## Final acceptance review - -The final native run passes 835 tests in 7 minutes 44 seconds on Dart 3.13.3, -SQLite 3.53.4 and PostgreSQL 18.4. The exact log, command and changed-source hashes -are saved in [native validation](../research/validation/native.json). Subsequent -documented usage and analysis passed without modifying the tested runtime. - -- Advanced queries are documented with an executable cookbook: filters, joined - ordering, relation counts, grouped CTEs, windows, subqueries and versioned - nullable cursors pass nine checks on each real backend. -- Catalog review now preserves PostgreSQL policy roles/command/mode/expressions - and enabled/forced RLS flags. Import/baseline leave them unchanged. Actual - triggers on both backends continue to execute after import and verification. -- Type, backend and migration behavior are reconciled against design §§5 and - 10–12 in [capabilities](capabilities.md). Calendar SQL arithmetic, timezone-rule - conversion and additional native extensions remain outside the implemented - typed API; the design does not specify that function family. -- Native Flutter has direct Android debug → release upgrade and restart evidence. - Browser JS/WASM have separate worker/OPFS evidence. Neither capture certifies - every platform or every native server test on that platform. -- Generation, editor and runtime cost reports retain their actual sample scopes - and historical source revisions. The same-session editor rename timeout and - checked server-restart recovery are retained as an explicit limitation. -- The documented onboarding, migration assets, examples and static analysis pass; - [acceptance](acceptance.md#final-lifecycle-audit) maps the complete usage lifecycle. - -To-one projections join by default when declared keys prove uniqueness; otherwise -they batch and check cardinality. Collections use explicit parameter-aware batches. -`verifyColumns` checks column names/types/nullability, integer widths, decimal precision/scale, SQLite collations and computed expressions/modes. `verifySchema` additionally compares defaults, -keys, row CHECK constraints and simple indexes; its `unmanaged` objects require separate review. -Ordinary migration batches are atomic. Explicit backfills use durable per-step -checkpoints and short data transactions on both databases. General recoverable -autocommit SQL remains PostgreSQL-only; SQLite rejects `CheckedSql`. These are -documented backend boundaries, not silently substituted behavior. - -The full suite includes 52 shared SQLite/PostgreSQL query checks, 20 generated -client/migration integration checks, 29 source generation checks, five codec -regressions, 19 migration evolution/catalog checks, 13 recovery checks, three CLI -workflows, 37 streaming/execution checks, 28 relation strategy checks and three -negative compilation checks covering 29 invalid API uses, plus 18 domain-codec -integration checks, 41 subscription checks, eight asset-builder checks and one -build_runner process workflow, plus 32 real-database set-query checks and 24 acquisition checks, plus 41 transaction-control checks, 29 retry checks, 19 application-version checks, 56 backfill checks, 18 catalog-import checks, two import CLI workflows, 20 integer-width checks, 29 exact-decimal checks, 19 decimal-precision checks, 23 decimal-division checks and 23 decimal-average checks, plus 24 local temporal checks, 20 UTC instant checks, 24 named SQL checks, two explicit floating-parameter checks, 24 unconstrained-relation checks, 17 CHECK-constraint checks and 17 client-default checks. - -Nineteen computed-column checks and additional generation/type checks are included -in the current aggregate above. - -## Environment - -Use the standalone SDK if the Flutter launcher tries to update its cache: -`/Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dart`. - -On 2026-09-15 the installed Flutter cache reports 3.47.4 stable with Dart 3.13.3. -Xcode reports 27.0 (27A266a), but `xcodebuild -license check` exits 69 and explicitly -reports that its license has not been accepted. Apple-platform Flutter verification -needs the user's own license review/acceptance and any remaining Xcode initialization. -No license was accepted on the user's behalf; other development can continue. - -The installed Android SDK and API 35 arm64 image were used to create a separate -temporary AVD without modifying the user's existing AVD. Both APK builds and the -three application phases succeeded. Flutter's build provisioned its NDK -28.2.13676358 through the existing SDK setup; no interactive license acceptance -was performed. Android acceptance does not depend on resolving the Xcode state. +## Completed refactor + +The entry APIs, explicit exports, internal module structure, public comments and +guides now form independent Dart libraries. There are no `part` or `part of` +directives in the package, examples, tools or tests. Typed declarations, +engine-specific Dart migration histories and independently usable layers remain +the foundation. + +- Implementation files are grouped under values, schema, query, runtime, ORM, + drivers, generation, migration and CLI responsibilities. Public entrypoints + explicitly select their exports; internal compiler and runtime bridges stay + outside the public namespace and generated API reference. +- `first()` requires a row. `firstOrNull()` and `singleOrNull()` explicitly permit + an empty result, including write-returning queries. A selected SQL NULL remains + a valid result. Cardinality checks on writes happen after execution; use a + transaction when an error must roll back the write. +- Schema declarations and physical metadata have separate exports. MySQL and + MariaDB have separate driver entrypoints. Existing connection factories remain + the direct way to open each engine. +- Public API comments explain ownership, errors and engine boundaries. Dartdoc + categories connect API pages to guides, with canonical library ownership and + CI checks for broken links and ambiguous exports. +- Archived exploration artifacts are removed. Benchmark and Flutter report + defaults use ignored `.dart_tool/benchmarks/` and `.dart_tool/flutter/` paths. + +## Published baseline + +`6.0.0-beta.1` is the released baseline at `5e4854df`, merged through +[PR #486](https://github.com/medz/dart-orm/pull/486). Its release CI passed 1141 native +tests with 41 capability skips and Chrome JS/WASM checks. SQLite, PostgreSQL, +MySQL 8.4 and MariaDB 11.8 formed the real-database scope. Flutter Web release +configurations and Android APK upgrade/restart were checked separately. + +The [package release](https://pub.dev/packages/orm/versions/6.0.0-beta.1) remains +immutable. Unpublished refactoring does not change that archive. Platform and +engine restrictions remain documented in [capabilities](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md). + +## Refactor validation + +Validated locally with Dart 3.13.3, Flutter 3.47.4 and Chrome 153: + +- Package analysis and formatting passed; the Flutter example also passed + `flutter analyze`. AST checks reject `part` directives and enforce layer + boundaries. Public namespace checks verify explicit exports. +- The complete native test run used real SQLite, PostgreSQL 18, MySQL 8.4 and + MariaDB 11.8: 1209 passed, 8 capability skips and 2 failures. Both failures were + the SQLite/PostgreSQL executions of an old upsert fixture expecting nullable + `first()`. After migrating it to `firstOrNull()`, the complete database, + cardinality, migration, export and layer suites passed again: 114 tests, zero + failures. These are separate runs, not a single all-green full-suite run. +- The native run includes generation, CLI, migration recovery, transactions and + database integration suites. The dedicated cardinality suite covers empty + results, selected NULLs, limits, write-returning behavior and rollback. +- Chrome JS and WASM each passed 21 browser scenarios. Flutter Web passed the + same 21 scenarios in JS, WASM without isolation and WASM with isolation. These + include OPFS persistence, reload recovery, nested-route asset resolution, + transactions, worker responsiveness and real Flutter frame progress. +- The main example ran successfully. The query cookbook passed all 9 checks on + both SQLite and PostgreSQL. Committed SQLite worker/WASM fingerprints match + the refactored source. +- `dart doc --validate-links` generated 21 public libraries with zero warnings + and zero errors. The generated index was checked for canonical API ownership + and absence of internal compiler/session helpers. +- Publication dry-run inspected the package archive, excluding generated docs, + local caches, repository test tools and removed artifacts. After committing, + `dart pub publish --dry-run` passed with zero warnings. + +Android, iOS, native macOS/Windows/Linux Flutter applications and non-Chrome +browsers were not rerun for this refactor. Earlier Android release evidence +belongs to the published baseline. Local logs and browser reports live under +ignored `.dart_tool/` directories; they are not published package contents. + +The refactor is prepared for `6.0.0-beta.2`. Publication follows Codex review and +CI approval of the release PR. The beta.1 archive above is unchanged. + +See [validation](https://github.com/medz/dart-orm/blob/main/doc/acceptance.md) and [contributing](https://github.com/medz/dart-orm/blob/main/doc/contributing.md) for commands. diff --git a/doc/queries.md b/doc/queries.md index 04ac6285..f032df44 100644 --- a/doc/queries.md +++ b/doc/queries.md @@ -8,7 +8,7 @@ after reading the selected columns. `(u.id, u.email).row` returns a positional `query.map(...)` maps the already selected Dart result. It does not create SQL columns, and its properties are not usable in SQL filters or ordering. -Run the [cookbook](../example/queries.dart) with `dart run example/queries.dart`. +Run the [cookbook](https://github.com/medz/dart-orm/blob/main/example/queries.dart) with `dart run example/queries.dart`. It checks nine combined query/schema scenarios on an in-memory SQLite database. Set `ORM_EXAMPLE_POSTGRES` to a disposable local PostgreSQL URL to run the same checks there. That example explicitly disables TLS for local development, creates @@ -34,11 +34,22 @@ An empty `isIn([])` is false. Other NULL-containing membership expressions retai SQL's three-valued logic, not Dart collection semantics. Values are bound as parameters. `like` treats `%` and `_` as SQL wildcards. -`get()` returns a list, `first()` returns the first value or null, and `single()` -requires exactly one result. `count()` and `exists()` issue dedicated SQL. Add -explicit ordering when the first row or page must be deterministic. Use -[keyset cursors](#keyset-pagination) for changing large datasets; -`skip`/`take` provide offset/limit pagination. +Choose the terminal operation that expresses the expected result count: + +| Operation | Empty result | Multiple results | +| --- | --- | --- | +| `get()` | Empty list | All selected rows | +| `first()` | Throws | First selected value | +| `firstOrNull()` | `null` | First selected value | +| `single()` | Throws | Throws | +| `singleOrNull()` | `null` | Throws | + +A nullable scalar selection can itself contain `null`; a nullable terminal does +not distinguish that value from an absent row. Select a Record containing a +non-null key when the distinction matters. `count()` and `exists()` issue +dedicated SQL. Add explicit ordering when the first row or page must be +deterministic. Use [keyset cursors](#keyset-pagination) for changing large +datasets; `skip`/`take` provide offset/limit pagination. ## Keyset pagination @@ -70,7 +81,7 @@ ordering do not have the required table-key proof and are rejected. ## Join and order by related fields Use a typed alias when a related field participates in root ordering or a flat -projection. This example uses the [generated example schema](../example/schema.dart): +projection. This example uses the [generated example schema](https://github.com/medz/dart-orm/blob/main/example/schema.dart): ```dart final author = usersTable.alias(); @@ -90,7 +101,7 @@ be reused across unrelated query scopes or referenced before their join. To order by a collection count, use `db.users.orderBy((u) => [u.posts.count().desc(), u.id.asc()])`. This compiles a correlated count without loading posts. Normal nested results still use -[relationship selections](relations.md) and their explicit loading strategies. +[relationship selections](https://github.com/medz/dart-orm/blob/main/doc/relations.md) and their explicit loading strategies. ## Grouping, subqueries, CTEs and windows @@ -122,7 +133,7 @@ to the outer query's fields through an explicitly captured expression. Queries from different database/transaction objects cannot be combined. For SQL beyond these typed operations, use parameterized raw expressions or -[checked named SQL](named-sql.md). Raw fragments are trusted SQL supplied by the +[checked named SQL](https://github.com/medz/dart-orm/blob/main/doc/named-sql.md). Raw fragments are trusted SQL supplied by the application; do not interpolate user input into them. ## UNION and UNION ALL @@ -225,4 +236,4 @@ parsed exactly, with overflow or fractional values rejected rather than rounded through a double. On `int`/`double` expressions, `average()` returns an approximate `double?`. On `Decimal` expressions, `average(scale: ..., rounding: ...)` returns an exact `Decimal?` rounded once to the requested scale, including when the total -would overflow. See [decimal arithmetic and averages](decimals.md). +would overflow. See [decimal arithmetic and averages](https://github.com/medz/dart-orm/blob/main/doc/decimals.md). diff --git a/doc/relations.md b/doc/relations.md index b9e96155..30b25521 100644 --- a/doc/relations.md +++ b/doc/relations.md @@ -58,7 +58,7 @@ cross-database queries or arbitrary relationship predicate declarations. ## Many-to-many with business fields An explicit association table gives each membership its own role and joining time. -The [teams example](../example/teams/schema.dart) uses two foreign keys and a +The [teams example](https://github.com/medz/dart-orm/blob/main/example/teams/schema.dart) uses two foreign keys and a composite primary key, with no artificial membership ID: ```dart @@ -238,9 +238,9 @@ should be excluded. `compile()` exposes the root SQL, including selected to-one JOINs. It does not pretend to contain the later collection statements. `inspect()` also describes conditional child SQL templates, output/key slots, joins and batch capacity; -see [plans and observations](observability.md). `onQuery` reports every +see [plans and observations](https://github.com/medz/dart-orm/blob/main/doc/observability.md). `onQuery` reports every executed statement and fetched row count. Streaming uses these same strategies -per root batch; see [execution](execution.md). +per root batch; see [execution](https://github.com/medz/dart-orm/blob/main/doc/execution.md). Multiple statements can observe different snapshots under PostgreSQL Read Committed. Use an explicit Repeatable Read transaction when root and collection diff --git a/doc/sqlite-web.md b/doc/sqlite-web.md index 72d6cc4a..b144f47e 100644 --- a/doc/sqlite-web.md +++ b/doc/sqlite-web.md @@ -42,7 +42,7 @@ URL configuration. `flutter run -d chrome`, `flutter build web` and is compiled once by the package maintainer. Both application compilation modes use that JavaScript worker with the same SQLite WASM engine. -See the runnable [Flutter example](../example/flutter/README.md). This packaging +See the runnable [Flutter example](https://github.com/medz/dart-orm/blob/main/example/flutter/README.md). This packaging uses Flutter's platform asset filters, available since Flutter 3.41. The ORM stays a pure Dart package without an SDK Flutter dependency. @@ -182,14 +182,9 @@ checks generated fixture freshness, compiles the client, starts a local asset server, and launches Chrome with a disposable profile. Set `CHROME_EXECUTABLE` to override its path. It writes `report.js.json` or `report.wasm.json` under `.dart_tool/browser/` and fails if the -browser reports any failed check. Reports contain the actual browser, Dart SDK, -compilation mode and engine checksum. The captured results are in -`research/validation/browser.json`. - -The subsequent [API audit capture](../research/validation/api-audit.md) passes -20 checks in each compilation mode, adding mixed-session query rejection, -independent optional-join guards and rejected-write state checks. Its logs are -separate from the original browser capture. +browser reports any failed check. Reports contain the actual browser, Dart SDK, compilation mode and engine +checksum. Consult [progress](https://github.com/medz/dart-orm/blob/main/doc/progress.md) for the revision and configurations +most recently verified. Run these separately from the native full suite: concurrent Dart CLI invocations can race while rewriting/codesigning the shared macOS native-asset cache. The @@ -206,8 +201,7 @@ throughput or establishes Safari, Firefox, IndexedDB or multi-tab service suppor The unified-entry acceptance adds actual Flutter release builds in JS and WASM, with and without cross-origin isolation headers. The app consumes the package's unchanged asset declarations and default URI resolution from a nested route. -It also checks stale worker rejection and WASM integrity failure. See -[the refactor validation](../research/validation/sqlite-refactor.md). +It also checks stale worker rejection and WASM integrity failure. Hot reload retains application state. Flutter Web hot restart, which can abandon live JS resources without closing them, is not certified by these release tests; diff --git a/doc/types.md b/doc/types.md index e07966ff..252d82b5 100644 --- a/doc/types.md +++ b/doc/types.md @@ -45,8 +45,8 @@ Imported types, extension types, public record aliases and nested generic types retain qualified names in generated code. Two different libraries may both declare `Email`. Output can be generated into a different directory. -See the complete [domain fixture](../test/support/codecs/schema.dart) and its -[types](../test/support/codecs/types.dart). `Codec.map` remains available when +See the complete [domain fixture](https://github.com/medz/dart-orm/blob/main/test/support/codecs/schema.dart) and its +[types](https://github.com/medz/dart-orm/blob/main/test/support/codecs/types.dart). `Codec.map` remains available when constructing table definitions directly at runtime. ## Enums @@ -158,7 +158,7 @@ normal reviewed migration diff/conversion workflow. Storage semantics still belong to each database. In particular, SQLite `bigint` text retains exact digits but does not provide numeric text ordering/arithmetic. Native enum types remain a backend extension. Browser numeric limits and tested -transport are described in [SQLite web](sqlite-web.md). Use `Decimal` for exact +transport are described in [SQLite web](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md). Use `Decimal` for exact decimal data. ## UTC instants @@ -193,8 +193,7 @@ on native Dart, not JavaScript or browser storage. Native drivers advertise `Capabilities.temporal`, covering the local types below and UTC instants. Borrowed PostgreSQL pools must use `postgresTypeRegistry()` and `PostgresDriver.borrow(pool, temporal: true)`. Without that capability, typed -temporal queries fail before execution. This replaces the earlier preview's -`localTemporal` flag; no compatibility alias is provided. +temporal queries fail before execution. ### Upgrading historical timestamp storage @@ -229,10 +228,10 @@ instants, so the shown conversion preserves values; existing infinities or value beyond DateTime's range still require separate data review. Old cursor tokens carry the old storage tag and cannot be reused under the new ordering contract. -The [captured legacy migration](../test/support/instants/m0001_legacy.dart) is tested -against its checksum from commit `00853b4`, including upgrade and rollback on real -databases. Plain unmanaged SQLite TEXT still imports as String; the managed -instant collation lets the importer infer DateTime without sampling data. +The [migration fixture](https://github.com/medz/dart-orm/blob/main/test/support/instants/m0001_legacy.dart) +illustrates a fixed timestamp history for upgrade and rollback checks. Plain +unmanaged SQLite TEXT imports as String; the managed instant collation lets the +importer infer DateTime without sampling data. ## Local calendar values @@ -312,7 +311,7 @@ reviewed migration; newly equivalent unique keys can make conversion fail and roll back. Historical backfills persist canonical text keys for resumable paging. SQL calendar arithmetic and timezone-rule conversion have no typed API in this -version; they are separate [extension boundaries](capabilities.md#values-and-physical-storage). +version; they are separate [extension boundaries](https://github.com/medz/dart-orm/blob/main/doc/capabilities.md#values-and-physical-storage). Use local types when the domain value actually has no timezone; use DateTime for resolved instants. @@ -441,7 +440,7 @@ verifying the historical schema. Native JIT and AOT checks include integer values beyond JavaScript's exact-number range. Browser int values use the safe-number range; verified wider transport -uses BigInt. See [SQLite web](sqlite-web.md) for the boundaries and browser checks. +uses BigInt. See [SQLite web](https://github.com/medz/dart-orm/blob/main/doc/sqlite-web.md) for the boundaries and browser checks. ## Verification @@ -456,7 +455,7 @@ program. These checks do not establish browser support. ## Exact decimals Use `Decimal` for finite base-ten values, including money. Generated fields use -`Codecs.decimal` without an annotation. See [exact decimals](decimals.md) for +`Codecs.decimal` without an annotation. See [exact decimals](https://github.com/medz/dart-orm/blob/main/doc/decimals.md) for construction, arithmetic, numeric keys, SQLite storage requirements and current limits. `BigInt` storage does not provide these fractional or SQLite numeric ordering semantics. diff --git a/doc/watch.md b/doc/watch.md index 1c379de5..7ee8eec7 100644 --- a/doc/watch.md +++ b/doc/watch.md @@ -146,6 +146,6 @@ Controlled completed-read and commit-acknowledgement faults exercise race handli against real database state. Negative compilation checks verify subscription result types. The native execution example also verifies subscriptions in macOS AOT. Real Chrome JavaScript and Dart WASM runs verify worker subscriptions; -the [native Flutter application](flutter.md) also verifies initial delivery, +the [native Flutter application](https://github.com/medz/dart-orm/blob/main/doc/flutter.md) also verifies initial delivery, committed relation writes, rollback without notification and typed patch refresh inside an Android AOT release application. diff --git a/example/flutter/README.md b/example/flutter/README.md index a6e1d058..a740fca1 100644 --- a/example/flutter/README.md +++ b/example/flutter/README.md @@ -18,7 +18,7 @@ they do not implement database drivers. The automated release runner is `dart run tool/test_flutter_web.dart /path/to/flutter` from the repository root. For native directory choices and current platform limits, -see [SQLite setup](../../doc/sqlite-web.md). +see [Flutter integration](../../doc/flutter.md) and [SQLite Web](../../doc/sqlite-web.md). ## Android acceptance @@ -59,9 +59,9 @@ The example uses Flutter's debug signing key for both APKs so `adb install -r` can preserve the database. These APKs are test artifacts, not store releases. The runner force-stops between phases, verifies distinct process IDs and matching database paths, compares rows and migration checksums after restart, and saves a -machine-readable report at `research/validation/flutter.json`. Logs, individual +machine-readable report at `.dart_tool/flutter/flutter.json`. Logs, individual reports and APK copies stay in `.dart_tool/flutter/`; the final screenshot is -saved as `research/validation/flutter-android.png`. +saved as `.dart_tool/flutter/flutter-android.png`. The application prints chunked base64 JSON so the host can capture release reports without `run-as`. SQL work runs while a Flutter animation and Dart timer advance; diff --git a/lib/builder.dart b/lib/builder.dart index e0e727b9..e58dc6d4 100644 --- a/lib/builder.dart +++ b/lib/builder.dart @@ -1,4 +1,11 @@ -/// Optional build_runner integration. Runtime clients import orm.dart instead. +/// Optional build_runner factories for model and named SQL generation. +/// +/// Select source libraries with `generate_for` in `build.yaml`. Applications +/// import the generated files; these builders run only during development. +/// +/// {@category Tooling} +/// {@canonicalFor build.ormBuilder} +/// {@canonicalFor build.ormQueryBuilder} library; -export 'src/generate.dart' show ormBuilder, ormQueryBuilder; +export 'src/generate/build.dart' show ormBuilder, ormQueryBuilder; diff --git a/lib/cli.dart b/lib/cli.dart index 6460687f..1e41e2d2 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -1,251 +1,16 @@ -/// Project commands with ordinary typed Dart configuration and static history. +/// Project commands configured with ordinary Dart and static migration history. +/// +/// Define [OrmConfig] in `orm.config.dart` and pass it to [runOrmCli]. +/// Generation works offline; database commands use the explicit connection +/// factory and the database engine fixed by the migration history. +/// +/// {@category Tooling} +/// {@canonicalFor config.OrmConfig} +/// {@canonicalFor runner.runOrmCli} library; -import 'dart:convert'; -import 'dart:io'; - -import 'package:dart_style/dart_style.dart'; -import 'package:path/path.dart' as p; - -import 'drivers/mysql.dart'; -import 'drivers/postgres.dart'; -import 'drivers/sqlite.dart'; -import 'generate.dart'; -import 'migrate.dart'; -import 'migrate_cli.dart'; -import 'runtime.dart'; -import 'src/sqlite/assets_io.dart'; - +export 'src/cli/config.dart' show OrmConfig; +export 'src/cli/runner.dart' show runOrmCli; export 'migrate.dart' show MigrationHistory, SchemaRenames, SchemaSnapshot; export 'migrate_cli.dart' show MigrationConnection; export 'runtime.dart' show SqlDatabase; - -part 'src/cli/commands.dart'; -part 'src/cli/init.dart'; - -final class OrmConfig { - final String schema; - final String? output; - final String migrations; - final MigrationHistory history; - final SchemaSnapshot snapshot; - final MigrationConnection? connect; - final SchemaRenames renames; - final Map> using; - const OrmConfig({ - this.schema = 'lib/schema.dart', - this.output, - this.migrations = 'migrations', - required this.history, - required this.snapshot, - this.connect, - this.renames = const SchemaRenames(), - this.using = const {}, - }); -} - -const _help = { - '': '''Usage: dart run orm [--json] [--config orm.config.dart] - init --database sqlite|postgres|mysql|mariadb - generate [schema.dart] [output.orm.dart] - migrate create|check|plan|apply|status|verify|baseline|record|inspect - migration registry [--dialect engine] - db inspect|import - queries generate|check - web-assets [directory] - -Run help for details. Project commands use orm.config.dart. -Exit codes: 0 success, 1 execution failure, 2 drift/import issues, 64 usage error. ---json emits machine reports; schema and migration files always remain Dart.''', - 'init': '''Usage: dart run orm init --database sqlite|postgres|mysql|mariadb [--json] -Creates lib/schema.dart, its generated client/snapshot, a static migration -registry and orm.config.dart in an existing Dart project. Never replaces files, -connects to a database or applies DDL. Server URLs are read from DATABASE_URL -only when a connection command runs.''', - 'generate': - '''Usage: dart run orm generate [schema.dart] [output.orm.dart] [--json] -Without a path, uses OrmConfig.schema or lib/schema.dart. Generates the client -and standalone physical snapshot without connecting to a database. -Use an explicit schema path to recreate a deleted snapshot before loading config.''', - 'migrate': '''Usage: dart run orm migrate [--json] - create [--allow-destructive] Generate schema, then save a reviewed diff - check Validate fixed Dart migration history - plan Inspect pending steps without applying - apply [--max-backfill-batches n] Apply pending migrations - status Inspect applied history and checkpoints - verify Compare the catalog with the bundled snapshot - baseline Verify and register an existing schema - record Record an unpublished migration edit - inspect Inspect physical table metadata -Configuration, fixed engine, connections, renames and conversions belong in -orm.config.dart. create/check/record do not connect. No command except explicit -apply/baseline changes database state. Review generated migrations before apply.''', - 'migration': '''Usage: dart run orm migration registry [--dialect sqlite|postgres|mysql|mariadb] -Rebuild static imports without updating reviewed migration fingerprints.''', - 'db': '''Usage: dart run orm db inspect --table name - dart run orm db import --output schema.dart [--table name] -Database: --sqlite file | --postgres-env NAME | --mysql-env NAME | --mariadb-env NAME -Server options: --tls verifyFull|require|disable; PostgreSQL: --database-schema name -Inspection and catalog import do not apply DDL or replace output files.''', - 'queries': '''Usage: dart run orm queries generate [output.queries.dart] - dart run orm queries check --source queries.dart [--output file] -Database options match help db. Generation is offline; check validates queries -against the selected database.''', - 'web-assets': '''Usage: dart run orm web-assets [directory] [--json] -Copies verified SQLite worker/WASM assets to web/orm by default. -Flutter Web bundles these resources automatically.''', -}; - -/// Called by the package executable or a project's explicit Dart entrypoint. -/// Loading a config is normal Dart execution, never reflection or JSON loading. -Future runOrmCli(List arguments, {OrmConfig? config}) async { - final json = arguments.contains('--json'); - final output = _CliOutput(json); - try { - final args = List.of(arguments); - if (args.where((a) => a == '--json').length > 1) { - throw const FormatException('Repeated option --json.'); - } - args.remove('--json'); - String? configPath; - final configIndex = args.indexOf('--config'); - if (configIndex >= 0) { - if (config != null || - args.where((a) => a == '--config').length > 1 || - configIndex + 1 >= args.length || - args[configIndex + 1].startsWith('--')) { - throw const FormatException( - 'Use --config once.', - ); - } - configPath = args[configIndex + 1]; - args.removeRange(configIndex, configIndex + 2); - } - if (args.isEmpty || - args.first == 'help' || - args.contains('--help') || - args.length == 1 && args.first == 'migrate') { - final topic = args.isEmpty || args.first == '--help' - ? '' - : args.first == 'help' - ? (args.length > 1 ? args[1] : '') - : args.first; - final help = _help[topic]; - if (help == null) throw FormatException('Unknown command: $topic.'); - if (json) { - output.report({'help': help}); - } else { - stdout.writeln(help); - } - return; - } - if (args.first == 'init') { - if (config != null || configPath != null) { - throw const FormatException( - 'Run init from the package CLI without --config.', - ); - } - await _initializeProject(args.skip(1).toList(), output); - return; - } - final projectCommand = - args.first == 'migrate' || args.first == 'generate' && args.length == 1; - if (config == null && projectCommand) { - final path = configPath ?? 'orm.config.dart'; - if (await File(path).exists()) { - final child = await Process.start(Platform.resolvedExecutable, [ - 'run', - path, - ...args, - if (json) '--json', - ], mode: ProcessStartMode.inheritStdio); - exitCode = await child.exitCode; - return; - } - if (configPath != null || args.first == 'migrate') { - throw FormatException( - 'Missing $path. Run dart run orm init --database .', - ); - } - args.add('lib/schema.dart'); - } else if (configPath != null) { - throw const FormatException( - '--config applies to generate without a path and migrate commands.', - ); - } - if (config != null && args.first == 'generate' && args.length == 1) { - await writeGeneratedSchema(config.schema, output: config.output); - output.report({ - 'generated': - config.output ?? p.setExtension(config.schema, '.orm.dart'), - }); - return; - } - if (config != null && args.first == 'migrate') { - var snapshot = config.snapshot; - if (args.length > 1 && args[1] == 'create') { - if (!(args.length == 3 || - args.length == 4 && args.last == '--allow-destructive') || - args[2].startsWith('--')) { - throw const FormatException( - 'Use migrate create [--allow-destructive].', - ); - } - // Generate before diffing so an edited model cannot silently use the - // snapshot compiled into this invocation's configuration. - final generated = await generateSchema( - config.schema, - outputPath: config.output, - ); - final client = - config.output ?? p.setExtension(config.schema, '.orm.dart'); - await File(client).parent.create(recursive: true); - await File(client).writeAsString(generated.dart); - final snapshotPath = client.endsWith('.orm.dart') - ? '${client.substring(0, client.length - '.orm.dart'.length)}.snapshot.dart' - : p.setExtension(client, '.snapshot.dart'); - await File(snapshotPath).writeAsString(generated.snapshotDart); - snapshot = generated.snapshot; - } - await runMigrationCli( - args.skip(1).toList(), - history: config.history, - directory: config.migrations, - schema: snapshot, - connect: config.connect, - renames: config.renames, - using: config.using, - json: json, - ); - return; - } - await _runExplicitCli(args, json: json); - } on FormatException catch (error) { - output.error(error.message, 64); - } on ArgumentError catch (_) { - output.error('Invalid command option or configuration. Use --help.', 64); - } catch (error) { - output.error(error.toString(), 1); - } -} - -final class _CliOutput(final bool json) { - void report(Map value) { - if (json) { - stdout.writeln(const JsonEncoder.withIndent(' ').convert(value)); - } else { - for (final entry in value.entries) { - stdout.writeln( - '${entry.key}: ${entry.value is String ? entry.value : jsonEncode(entry.value)}', - ); - } - } - } - - void error(String message, int code) { - stderr.writeln( - json ? jsonEncode({'error': message, 'exitCode': code}) : message, - ); - exitCode = code; - } -} diff --git a/lib/driver.dart b/lib/driver.dart index 84848dd1..f471ca9a 100644 --- a/lib/driver.dart +++ b/lib/driver.dart @@ -1,201 +1,50 @@ -/// Raw SQL driver contracts. Independent of query builders and ORM sessions. +/// Raw SQL connections, bound commands, results, and execution controls. +/// +/// A [Driver] leases a [SqlConnection] for an awaited callback. [SqlCommand] +/// keeps SQL text separate from values; [SqlResult] exposes raw rows without +/// generated model decoding. [Capabilities] reports the operations the concrete +/// engine and adapter can perform. +/// +/// Concrete drivers are available from `package:orm/drivers/sqlite.dart`, +/// `package:orm/drivers/postgres.dart`, `package:orm/drivers/mysql.dart`, and +/// `package:orm/drivers/mariadb.dart`. Import `package:orm/runtime.dart` when you +/// also need managed sessions and transactions. +/// +/// {@category Drivers} +/// {@canonicalFor driver.AcquisitionOptions} +/// {@canonicalFor driver.Backend} +/// {@canonicalFor driver.CancellationToken} +/// {@canonicalFor driver.Capabilities} +/// {@canonicalFor driver.Driver} +/// {@canonicalFor driver.ExecutionOptions} +/// {@canonicalFor driver.Mariadb} +/// {@canonicalFor driver.Mysql} +/// {@canonicalFor driver.Postgres} +/// {@canonicalFor driver.SqlCommand} +/// {@canonicalFor driver.SqlConnection} +/// {@canonicalFor driver.SqlCursor} +/// {@canonicalFor driver.SqlDialect} +/// {@canonicalFor driver.SqlFailure} +/// {@canonicalFor driver.SqlResult} +/// {@canonicalFor driver.Sqlite} library; -import 'values.dart'; export 'values.dart'; - -enum SqlDialect { sqlite, postgres, mysql, mariadb } - -/// SQL text and separately bound values. Values are never interpolated into SQL. -final class SqlCommand { - final String sql; - final List parameters; - SqlCommand(this.sql, [List parameters = const []]) - : parameters = List.unmodifiable(parameters); -} - -sealed class Backend { - const Backend(); -} - -final class Postgres extends Backend { - const Postgres(); -} - -final class Sqlite extends Backend { - const Sqlite(); -} - -final class Mysql extends Backend { - const Mysql(); -} - -final class Mariadb extends Backend { - const Mariadb(); -} - -final class Capabilities { - final SqlDialect dialect; - final int maxParameters; - final bool returning; - final bool windowFunctions; - final bool streaming; - final bool cancellation; - - /// Honors statement deadlines, potentially by discarding the connection. - /// A timeout does not prove that a submitted write was rolled back. - final bool statementTimeout; - final bool exactDecimal; - final bool temporal; - const Capabilities({ - required this.dialect, - required this.maxParameters, - this.returning = true, - this.windowFunctions = true, - this.streaming = false, - this.cancellation = false, - bool? statementTimeout, - this.exactDecimal = false, - this.temporal = false, - }) : statementTimeout = statementTimeout ?? cancellation; -} - -final class SqlResult { - final List> rows; - final List columns; - final int affectedRows; - - /// Generated identity returned directly by the server, when available. - final int? lastInsertId; - const SqlResult( - this.rows, { - this.columns = const [], - this.affectedRows = 0, - this.lastInsertId, - }); -} - -abstract interface class SqlConnection { - /// Actual adapter state after the last completed request; null if unavailable. - bool? get transactionActive; - Future execute( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }); - Future openCursor( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }); - - /// Permanently discard a connection whose protocol or transaction is uncertain. - Future invalidate(); -} - -abstract interface class Driver { - Capabilities get capabilities; - Future run(Future Function(SqlConnection) action); - Future close(); -} - -/// A cancellation request. Await the operation itself to observe its actual -/// database outcome; completion can win a race with cancellation. -final class CancellationToken { - bool _cancelled = false; - final Set _listeners = {}; - bool get isCancelled => _cancelled; - void cancel() { - if (_cancelled) return; - _cancelled = true; - for (final listener in _listeners.toList()) { - _notify(listener); - } - _listeners.clear(); - } - - /// Adapter hook; returns a function that removes the listener. - /// Listener errors are isolated so every pending operation receives cancellation. - void Function() listen(void Function() listener) { - if (_cancelled) { - _notify(listener); - return () {}; - } - _listeners.add(listener); - return () => _listeners.remove(listener); - } - - void _notify(void Function() listener) { - try { - listener(); - } catch (_) {} - } -} - -final class ExecutionOptions { - final CancellationToken? cancellation; - - /// Time allowed to acquire a lease. Independent of statement [timeout]. - final Duration? acquireTimeout; - - /// Limit for each database statement/fetch, not time spent consuming rows. - final Duration? timeout; - const ExecutionOptions({ - this.cancellation, - this.timeout, - this.acquireTimeout, - }); - AcquisitionOptions get acquisition => - acquireTimeout == null && cancellation == null - ? const AcquisitionOptions() - : AcquisitionOptions(timeout: acquireTimeout, cancellation: cancellation); - void check() { - if (acquireTimeout != null && acquireTimeout! <= Duration.zero) { - throw ArgumentError.value(acquireTimeout, 'acquireTimeout'); - } - if (timeout != null && timeout! <= Duration.zero) { - throw ArgumentError.value(timeout, 'timeout'); - } - if (cancellation?.isCancelled ?? false) { - throw const OrmException( - 'OPERATION.CANCELLED', - 'Cancelled before starting a database operation.', - ); - } - } -} - -/// Limits waiting for a connection, including establishment and initialization. -/// Cancellation after acquisition does not cancel the session callback. -final class AcquisitionOptions { - final Duration? timeout; - final CancellationToken? cancellation; - const AcquisitionOptions({this.timeout, this.cancellation}); - void check() { - if (timeout != null && timeout! <= Duration.zero) { - throw ArgumentError.value(timeout, 'acquireTimeout'); - } - if (cancellation?.isCancelled ?? false) { - throw const OrmException( - 'OPERATION.CANCELLED', - 'Connection acquisition cancelled.', - ); - } - } -} - -abstract interface class SqlCursor { - Future fetch( - int count, { - ExecutionOptions options = const ExecutionOptions(), - }); - Future close(); -} - -/// Adapter classification. Retrying also requires a confirmed rollback and an -/// explicitly repeatable application callback; these flags alone are not enough. -abstract interface class SqlFailure implements Exception { - bool get retryTransaction; - - /// Retry the same COMMIT only while the adapter confirms the transaction is active. - bool get retryCommit; - bool get commitRejected; -} +export 'src/driver/driver.dart' + show + AcquisitionOptions, + Backend, + CancellationToken, + Capabilities, + Driver, + ExecutionOptions, + Mariadb, + Mysql, + Postgres, + SqlCommand, + SqlConnection, + SqlCursor, + SqlDialect, + SqlFailure, + SqlResult, + Sqlite; diff --git a/lib/drivers/mariadb.dart b/lib/drivers/mariadb.dart index bb8771fb..00e62e21 100644 --- a/lib/drivers/mariadb.dart +++ b/lib/drivers/mariadb.dart @@ -1,5 +1,13 @@ /// MariaDB connections without ORM or generated model dependencies. -library; +/// +/// {@category Drivers} +/// {@canonicalFor driver.MariadbDriver} +/// {@canonicalFor options.MariadbOptions} +// Stable names distinguish raw drivers from ORM entrypoints in Dartdoc. +// ignore: unnecessary_library_name +library drivers_mariadb; export '../driver.dart'; -export 'mysql.dart' show MariadbDriver, MariadbOptions, MysqlTls, MysqlFailure; +export '../src/mysql/driver.dart' show MariadbDriver; +export '../src/mysql/options.dart' show MariadbOptions, MysqlTls; +export '../src/mysql/failure.dart' show MysqlFailure; diff --git a/lib/drivers/mysql.dart b/lib/drivers/mysql.dart index 1af68625..b0341a91 100644 --- a/lib/drivers/mysql.dart +++ b/lib/drivers/mysql.dart @@ -1,283 +1,18 @@ -/// MySQL and MariaDB connections without ORM or generated model dependencies. -library; +/// MySQL connections for raw SQL and typed ORM sessions. +/// +/// Each driver owns one physical connection and queues entire connection leases, +/// so another caller cannot enter an active transaction. +/// +/// {@category Drivers} +/// {@canonicalFor driver.MysqlDriver} +/// {@canonicalFor failure.MysqlFailure} +/// {@canonicalFor options.MysqlOptions} +/// {@canonicalFor options.MysqlTls} +// Stable names distinguish raw drivers from ORM entrypoints in Dartdoc. +// ignore: unnecessary_library_name +library drivers_mysql; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:mysql_client_plus/exception.dart' as mysql; -import 'package:mysql_client_plus/mysql_client_plus.dart' as mysql; -import 'package:mysql_client_plus/mysql_protocol.dart' as protocol; - -import '../driver.dart'; export '../driver.dart'; - -part '../src/mysql/connection.dart'; -part '../src/mysql/values.dart'; - -enum MysqlTls { - /// TLS with certificate and hostname verification. - verifyFull, - - /// TLS without certificate verification. Use only on a trusted network. - require, - - /// Unencrypted transport. The client requires TLS for SHA-2 authentication. - disable, -} - -class MysqlOptions { - final Uri url; - final MysqlTls tls; - final Duration connectTimeout; - final Duration queryTimeout; - final SecurityContext? securityContext; - - const MysqlOptions({ - required this.url, - this.tls = MysqlTls.verifyFull, - this.connectTimeout = const Duration(seconds: 10), - this.queryTimeout = const Duration(seconds: 30), - this.securityContext, - }); -} - -final class MariadbOptions extends MysqlOptions { - const MariadbOptions({ - required super.url, - super.tls, - super.connectTimeout, - super.queryTimeout, - super.securityContext, - }); -} - -/// A server-reported error. Numeric codes are preserved; SQLSTATE is not -/// exposed by mysql_client_plus and is not guessed from the error message. -final class MysqlFailure implements SqlFailure { - final mysql.MySQLServerException cause; - const MysqlFailure(this.cause); - - int get code => cause.errorCode; - @override - bool get retryTransaction => code == 1213 || code == 1205; - @override - bool get retryCommit => false; - @override - bool get commitRejected => code == 1213; - @override - String toString() => 'MysqlFailure($code): ${cause.message}'; -} - -/// One physical connection. Whole leases are queued, including transactions. -/// Create additional drivers explicitly when independent concurrent leases are -/// required. Closing drains all leases accepted before close was called. -final class MysqlDriver extends _MysqlDriver { - MysqlDriver._( - mysql.MySQLConnection connection, - MysqlOptions options, - String serverVersion, - ) : super(connection, options, serverVersion, SqlDialect.mysql); - - static Future open(MysqlOptions options) async { - final opened = await _openMysql(options, SqlDialect.mysql); - return MysqlDriver._(opened.connection, options, opened.version); - } -} - -final class MariadbDriver extends _MysqlDriver { - MariadbDriver._( - mysql.MySQLConnection connection, - MariadbOptions options, - String serverVersion, - ) : super(connection, options, serverVersion, SqlDialect.mariadb); - - static Future open(MariadbOptions options) async { - final opened = await _openMysql(options, SqlDialect.mariadb); - return MariadbDriver._(opened.connection, options, opened.version); - } -} - -abstract class _MysqlDriver implements Driver { - final mysql.MySQLConnection _connection; - final MysqlOptions _options; - final SqlDialect _dialect; - final String serverVersion; - Future _tail = Future.value(); - Future? _closing; - bool _discarded = false; - - _MysqlDriver( - this._connection, - this._options, - this.serverVersion, - this._dialect, - ); - - @override - Capabilities get capabilities => Capabilities( - dialect: _dialect, - maxParameters: 65535, - returning: false, - streaming: false, - cancellation: false, - statementTimeout: true, - exactDecimal: true, - temporal: true, - ); - - @override - Future run(Future Function(SqlConnection) action) { - if (_closing != null) { - return Future.error( - const OrmException( - 'DRIVER.CLOSED', - 'MySQL driver is closing or closed.', - ), - ); - } - final result = _tail.then((_) async { - if (_discarded || !_connection.connected) { - throw const OrmException( - 'DRIVER.CLOSED', - 'MySQL connection has been discarded. Open a new driver.', - ); - } - final lease = _MysqlConnection(_connection, _options.queryTimeout); - try { - final result = await action(lease); - await lease._release(); - return result; - } catch (error, stack) { - try { - await lease._release(); - } catch (_) { - await lease.invalidate(); - } - Error.throwWithStackTrace(error, stack); - } finally { - if (lease._invalid) _discarded = true; - } - }); - _tail = result.then((_) {}, onError: (Object _, StackTrace _) {}); - return result; - } - - @override - Future close() => _closing ??= _tail.then((_) async { - if (!_discarded && _connection.connected) { - try { - await _connection.close().timeout(_options.queryTimeout); - } catch (_) { - _connection.getSocket().destroy(); - rethrow; - } - } else { - _connection.getSocket().destroy(); - } - }); -} - -Future<({mysql.MySQLConnection connection, String version})> _openMysql( - MysqlOptions options, - SqlDialect dialect, -) async { - final url = options.url; - final schemes = dialect == SqlDialect.mysql - ? const {'mysql'} - : const {'mariadb', 'mysql'}; - if (!schemes.contains(url.scheme) || - url.host.isEmpty || - url.pathSegments.length != 1 || - url.pathSegments.single.isEmpty || - url.hasPort && (url.port < 1 || url.port > 65535) || - url.hasQuery || - url.hasFragment || - url.userInfo.isEmpty || - options.connectTimeout <= Duration.zero || - options.queryTimeout <= Duration.zero) { - throw ArgumentError( - 'Provide a ${dialect.name} URL with user and database, positive timeouts, ' - 'and typed options instead of URL query parameters.', - ); - } - final colon = url.userInfo.indexOf(':'); - final username = Uri.decodeComponent( - colon < 0 ? url.userInfo : url.userInfo.substring(0, colon), - ); - final password = colon < 0 - ? '' - : Uri.decodeComponent(url.userInfo.substring(colon + 1)); - if (username.isEmpty) throw ArgumentError('MySQL user must not be empty.'); - mysql.MySQLConnection? connection; - var expired = false; - try { - return await (() async { - final opened = await mysql.MySQLConnection.createConnection( - host: url.host, - port: url.hasPort ? url.port : 3306, - userName: username, - password: password, - databaseName: url.pathSegments.single, - secure: options.tls != MysqlTls.disable, - securityContext: options.securityContext, - // The upstream client's default accepts invalid certificates. - onBadCertificate: (_) => options.tls == MysqlTls.require, - ); - connection = opened; - if (expired) { - opened.getSocket().destroy(); - throw const OrmException('CONNECTION.TIMEOUT', 'Connection timed out.'); - } - await opened.connect( - timeoutMs: options.connectTimeout.inMilliseconds - .clamp(1, 2147483647) - .toInt(), - ); - final result = await opened.execute('SELECT VERSION()'); - final version = result.rows.single.colAt(0) as String; - final maria = version.toLowerCase().contains('mariadb'); - if (maria != (dialect == SqlDialect.mariadb)) { - throw OrmException( - 'DRIVER.ENGINE', - 'Expected ${dialect.name}, but the server reports $version.', - ); - } - final match = RegExp(r'^(\d+)\.(\d+)').firstMatch(version); - final major = match == null ? 0 : int.parse(match[1]!); - final minor = match == null ? 0 : int.parse(match[2]!); - if (maria - ? major < 10 || major == 10 && minor < 6 - : major < 8 || major == 8 && minor < 4) { - throw const OrmException( - 'DRIVER.VERSION', - 'MySQL 8.4+ or MariaDB 10.6+ is required.', - ); - } - // Do not silently coerce invalid dates, truncate values, or convert - // instants through the deployment host's time zone. - await opened.execute("SET SESSION time_zone = '+00:00'"); - await opened.execute( - "SET SESSION sql_mode = CONCAT_WS(',', NULLIF(@@SESSION.sql_mode, ''), " - "'ANSI_QUOTES', 'NO_BACKSLASH_ESCAPES', 'STRICT_ALL_TABLES', " - "'NO_ZERO_DATE', 'NO_ZERO_IN_DATE', 'ERROR_FOR_DIVISION_BY_ZERO', " - "'NO_ENGINE_SUBSTITUTION')", - ); - await opened.execute('SET SESSION autocommit = 1'); - return (connection: opened, version: version); - })().timeout(options.connectTimeout); - } on TimeoutException catch (error) { - expired = true; - connection?.getSocket().destroy(); - throw OrmException( - 'CONNECTION.TIMEOUT', - 'MySQL connection establishment timed out.', - cause: error, - ); - } catch (_) { - expired = true; - connection?.getSocket().destroy(); - rethrow; - } -} +export '../src/mysql/driver.dart' show MysqlDriver; +export '../src/mysql/failure.dart' show MysqlFailure; +export '../src/mysql/options.dart' show MysqlOptions, MysqlTls; diff --git a/lib/drivers/postgres.dart b/lib/drivers/postgres.dart index 83aade44..33eff783 100644 --- a/lib/drivers/postgres.dart +++ b/lib/drivers/postgres.dart @@ -1,454 +1,20 @@ -/// PostgreSQL connection pooling with backend-specific configuration. -library; +/// PostgreSQL connection pools for raw SQL runtimes and typed ORM sessions. +/// +/// [PostgresDriver] owns its pool by default. [PostgresDriver.borrow] uses a +/// caller-owned pool and leaves it open when the driver closes. +/// +/// {@category Drivers} +/// {@canonicalFor driver.PostgresDriver} +/// {@canonicalFor failure.PostgresFailure} +/// {@canonicalFor options.PostgresOptions} +/// {@canonicalFor options.PostgresTls} +/// {@canonicalFor temporal.postgresTypeRegistry} +// Stable names distinguish raw drivers from ORM entrypoints in Dartdoc. +// ignore: unnecessary_library_name +library drivers_postgres; -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:postgres/postgres.dart' as pg; - -import '../driver.dart'; export '../driver.dart'; - -part '../src/postgres/temporal.dart'; - -enum PostgresTls { verifyFull, require, disable } - -final class PostgresOptions { - final Uri url; - final PostgresTls tls; - final int maxConnections; - final Duration connectTimeout; - - /// Driver pool queue limit; separate from opening a new connection. - final Duration poolTimeout; - final Duration queryTimeout; - final String applicationName; - final String? schema; - const PostgresOptions({ - required this.url, - this.tls = PostgresTls.verifyFull, - this.maxConnections = 8, - this.connectTimeout = const Duration(seconds: 10), - this.poolTimeout = const Duration(seconds: 30), - this.queryTimeout = const Duration(seconds: 30), - this.applicationName = 'dart-orm', - this.schema, - }); -} - -/// A server-reported error, preserving SQLSTATE and the original exception. -final class PostgresFailure implements SqlFailure { - final pg.ServerException cause; - const PostgresFailure(this.cause); - String? get code => cause.code; - @override - bool get retryTransaction => code == '40001' || code == '40P01'; - @override - bool get retryCommit => false; - @override - bool get commitRejected => code?.startsWith('23') == true || retryTransaction; - @override - String toString() => cause.toString(); -} - -final class PostgresDriver implements Driver { - final pg.Pool _pool; - final bool _ownsPool; - final bool _temporal; - final Future Function()? _cancelConnection; - final Expando _backendIds = Expando(); - final Duration? _queryTimeout; - final Duration? _connectTimeout; - bool _closed = false; - - PostgresDriver(PostgresOptions options) - : _pool = _createPool(options), - _ownsPool = true, - _temporal = true, - _queryTimeout = options.queryTimeout, - _connectTimeout = options.connectTimeout, - _cancelConnection = (() => _openControl(options)); - PostgresDriver.borrow( - pg.Pool pool, { - Future Function()? cancellationConnection, - this._queryTimeout, - // Enable only when the pool uses postgresTypeRegistry(). - this._temporal = false, - }) : _pool = pool, - _ownsPool = false, - _connectTimeout = null, - _cancelConnection = cancellationConnection; - - static Future _openControl(PostgresOptions options) { - final url = options.url, colon = options.url.userInfo.indexOf(':'); - return pg.Connection.open( - pg.Endpoint( - host: url.host, - port: url.hasPort ? url.port : 5432, - database: url.pathSegments.single, - username: url.userInfo.isEmpty - ? null - : Uri.decodeComponent( - colon < 0 ? url.userInfo : url.userInfo.substring(0, colon), - ), - password: colon < 0 - ? null - : Uri.decodeComponent(url.userInfo.substring(colon + 1)), - ), - settings: pg.ConnectionSettings( - connectTimeout: options.connectTimeout, - queryTimeout: const Duration(seconds: 5), - sslMode: switch (options.tls) { - PostgresTls.verifyFull => pg.SslMode.verifyFull, - PostgresTls.require => pg.SslMode.require, - PostgresTls.disable => pg.SslMode.disable, - }, - applicationName: '${options.applicationName}/cancel', - ), - ); - } - - static pg.Pool _createPool(PostgresOptions options) { - final url = options.url; - if (!{'postgres', 'postgresql'}.contains(url.scheme) || - url.host.isEmpty || - url.pathSegments.length != 1 || - options.maxConnections < 1) { - throw ArgumentError('Provide a PostgreSQL URL and a positive pool size.'); - } - if (options.queryTimeout <= Duration.zero || - options.poolTimeout <= Duration.zero || - options.connectTimeout <= Duration.zero) { - throw ArgumentError('PostgreSQL timeouts must be positive.'); - } - if (url.hasQuery || url.hasFragment) { - throw ArgumentError( - 'Use typed PostgresOptions instead of URL query options.', - ); - } - final colon = url.userInfo.indexOf(':'); - final user = colon < 0 ? url.userInfo : url.userInfo.substring(0, colon); - final password = colon < 0 - ? null - : Uri.decodeComponent(url.userInfo.substring(colon + 1)); - return pg.Pool.withEndpoints( - [ - pg.Endpoint( - host: url.host, - port: url.hasPort ? url.port : 5432, - database: url.pathSegments.single, - username: user.isEmpty ? null : Uri.decodeComponent(user), - password: password, - ), - ], - settings: pg.PoolSettings( - maxConnectionCount: options.maxConnections, - connectTimeout: options.poolTimeout, - queryTimeout: null, - applicationName: options.applicationName, - timeZone: 'UTC', - typeRegistry: postgresTypeRegistry(), - onOpen: options.schema == null - ? null - : (connection) async { - final schema = '"${options.schema!.replaceAll('"', '""')}"'; - await connection.execute( - pg.Sql( - r"SELECT pg_catalog.set_config('search_path', $1, false)", - types: [pg.Type.text], - ), - parameters: [schema], - ); - }, - sslMode: switch (options.tls) { - PostgresTls.verifyFull => pg.SslMode.verifyFull, - PostgresTls.require => pg.SslMode.require, - PostgresTls.disable => pg.SslMode.disable, - }, - ), - ); - } - - @override - Capabilities get capabilities => Capabilities( - dialect: SqlDialect.postgres, - maxParameters: 65535, - streaming: true, - cancellation: _cancelConnection != null, - exactDecimal: true, - temporal: _temporal, - ); - @override - Future run(Future Function(SqlConnection) action) async { - if (_closed) { - throw const OrmException('DRIVER.CLOSED', 'PostgreSQL driver is closed.'); - } - var acquired = false; - try { - return await _pool.withConnection( - (connection) { - acquired = true; - return action( - _PostgresConnection( - connection, - _cancelConnection, - _backendIds, - _queryTimeout, - ), - ); - }, - settings: _connectTimeout == null - ? null - : pg.ConnectionSettings(connectTimeout: _connectTimeout), - ); - } on TimeoutException catch (error) { - if (acquired) rethrow; - throw OrmException( - 'CONNECTION.TIMEOUT', - 'PostgreSQL connection acquisition timed out.', - cause: error, - ); - } - } - - @override - Future close() async { - if (_closed) return; - _closed = true; - if (_ownsPool) await _pool.close(); - } -} - -final class _PostgresConnection implements SqlConnection { - // package:postgres does not expose ReadyForQuery transaction status publicly. - @override - bool? get transactionActive => null; - final pg.Connection connection; - final Future Function()? _control; - final Expando _backendIds; - final Duration? _queryTimeout; - bool _busy = false; - int _cursorId = 0; - _PostgresConnection( - this.connection, - this._control, - this._backendIds, - this._queryTimeout, - ); - - // ResultStream is buffered only for this bounded statement. Its subscription - // has no hidden timeout task that could cancel a later statement. We own and - // await cancellation below, including connection-control cleanup. - Future _query(SqlCommand command) async { - final statement = await connection.prepare( - pg.Sql( - command.sql, - types: List.filled(command.parameters.length, pg.Type.unspecified), - ), - ); - try { - final rows = []; - final subscription = statement - .bind([ - for (final p in command.parameters) p is SqlReal ? p.value : p, - ]) - .listen(rows.add); - try { - await subscription.asFuture(); - final schema = await subscription.schema; - final jsonColumns = { - for (final (index, column) in schema.columns.indexed) - if (column.typeOid == pg.Type.json.oid || - column.typeOid == pg.Type.jsonb.oid) - index, - }; - // The driver parses JSON strings and JSON null into ordinary Dart values. - // Retain their distinction from SQL text and SQL NULL without reserializing. - final List> values = jsonColumns.isEmpty - ? rows - : [ - for (final row in rows) - [ - for (var i = 0; i < row.length; i++) - jsonColumns.contains(i) && !row.isSqlNull(i) - ? SqlJson(row[i]) - : row[i], - ], - ]; - return SqlResult( - values, - columns: [for (final c in schema.columns) c.columnName ?? ''], - affectedRows: await subscription.affectedRows, - ); - } finally { - await subscription.cancel(); - } - } finally { - await statement.dispose(); - } - } - - @override - Future execute( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }) async { - options.check(); - if (_busy) { - throw const OrmException( - 'SESSION.BUSY', - 'Await the active statement before using this connection again.', - ); - } - final timeout = options.timeout ?? _queryTimeout; - if (timeout != null && timeout <= Duration.zero) { - throw ArgumentError.value(timeout, 'timeout'); - } - if ((options.cancellation != null || timeout != null) && _control == null) { - throw const OrmException( - 'CAPABILITY.CANCEL', - 'A borrowed PostgreSQL pool needs a cancellation connection factory.', - ); - } - _busy = true; - Future? cancelling; - var completed = false, timedOut = false, statementStarted = false; - Timer? deadline; - void Function()? unsubscribe; - try { - if (options.cancellation != null || timeout != null) { - var backendId = _backendIds[connection.info]; - void cancel() { - if (completed || cancelling != null) return; - final pid = backendId; - cancelling = () async { - // PID discovery is part of the same deadline. Until it completes, - // discard this connection; the application SQL has not been sent. - if (pid == null) { - await connection.close(force: true); - return; - } - try { - final control = await _control!(); - try { - if (!completed) { - final result = await control.execute( - pg.Sql( - r'SELECT pg_cancel_backend($1)', - types: [pg.Type.integer], - ), - parameters: [pid], - ); - if (result.single.single != true && !completed) { - await connection.close(force: true); - } - } - } finally { - await control.close(force: true); - } - } catch (_) { - await connection.close(force: true); - rethrow; - } - }(); - // Observe failures immediately; the operation awaits cleanup below. - unawaited(cancelling!.catchError((Object _) {})); - } - - unsubscribe = options.cancellation?.listen(cancel); - if (timeout != null) { - deadline = Timer(timeout, () { - timedOut = true; - cancel(); - }); - } - options.check(); - backendId ??= _backendIds[connection.info] = - (await _query(SqlCommand('SELECT pg_backend_pid()'))) - .rows - .single - .single - as int; - options.check(); - if (timedOut) { - throw const OrmException( - 'OPERATION.TIMEOUT', - 'PostgreSQL PID discovery exceeded the statement deadline.', - ); - } - } - statementStarted = true; - return await _query(command); - } catch (error) { - if ((timedOut || options.cancellation?.isCancelled == true) && - (!statementStarted || - error is pg.ServerException && error.code == '57014')) { - throw OrmException( - timedOut ? 'OPERATION.TIMEOUT' : 'OPERATION.CANCELLED', - statementStarted ? 'PostgreSQL stopped the statement.' : 'PostgreSQL PID discovery was interrupted before the statement.', - cause: error, - ); - } - if (error is pg.ServerException) throw PostgresFailure(error); - rethrow; - } finally { - completed = true; - deadline?.cancel(); - unsubscribe?.call(); - try { - await cancelling; - } finally { - _busy = false; - } - } - } - - @override - Future openCursor( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }) async { - final name = 'orm_cursor_${_cursorId++}'; - await execute( - SqlCommand( - 'DECLARE "$name" NO SCROLL CURSOR FOR ${command.sql}', - command.parameters, - ), - options: options, - ); - return _PostgresCursor(this, name); - } - - @override - Future invalidate() => connection.close(force: true); -} - -final class _PostgresCursor( - final _PostgresConnection connection, - final String name, -) implements SqlCursor { - bool _closed = false; - @override - Future fetch( - int count, { - ExecutionOptions options = const ExecutionOptions(), - }) { - if (_closed) throw const OrmException('CURSOR.CLOSED', 'Cursor has ended.'); - if (count < 1) throw ArgumentError.value(count, 'count'); - return connection.execute( - SqlCommand('FETCH FORWARD $count FROM "$name"'), - options: options, - ); - } - - @override - Future close() async { - if (_closed) return; - _closed = true; - try { - await connection.execute(SqlCommand('CLOSE "$name"')); - } on PostgresFailure catch (e) { - if (e.code != '25P02' && e.code != '34000') rethrow; - } - } -} +export '../src/postgres/driver.dart' show PostgresDriver; +export '../src/postgres/failure.dart' show PostgresFailure; +export '../src/postgres/options.dart' show PostgresOptions, PostgresTls; +export '../src/postgres/temporal.dart' show postgresTypeRegistry; diff --git a/lib/drivers/sqlite.dart b/lib/drivers/sqlite.dart index 3024bce6..7a1d2087 100644 --- a/lib/drivers/sqlite.dart +++ b/lib/drivers/sqlite.dart @@ -1,52 +1,20 @@ -/// SQLite with background execution on native platforms and the web. -library; - -import '../driver.dart'; -import '../src/sqlite/options.dart'; -import '../src/sqlite/unsupported.dart' - if (dart.library.io) '../src/sqlite/native.dart' - if (dart.library.js_interop) '../src/sqlite/web.dart'; +/// SQLite connections with background execution on native platforms and Web. +/// +/// [SqliteOptions.memory] works on both platforms. Persistent databases use a +/// native file path or a named browser database through [SqliteOptions.persistent]. +/// +/// {@category Drivers} +/// {@canonicalFor driver.SqliteDriver} +/// {@canonicalFor failure.SqliteFailure} +/// {@canonicalFor options.SqliteOptions} +/// {@canonicalFor options.SqliteWebOptions} +/// {@canonicalFor options.SqliteJournal} +// Stable names distinguish raw drivers from ORM entrypoints in Dartdoc. +// ignore: unnecessary_library_name +library drivers_sqlite; export '../driver.dart'; -export '../src/sqlite/failure.dart'; -export '../src/sqlite/options.dart'; - -final class SqliteDriver implements Driver { - final SqlConnection _connection; - final Future Function() _shutdown; - @override - final Capabilities capabilities; - Future _tail = Future.value(); - Future? _closing; - bool _closed = false; - SqliteDriver._(this._connection, this.capabilities, this._shutdown); - - static Future open(SqliteOptions options) async { - final opened = await connectSqlite(options); - if (opened.version < 3035000) { - await opened.close(); - throw const OrmException( - 'CAPABILITY.VERSION', - 'SQLite 3.35 or newer is required.', - ); - } - return SqliteDriver._(opened.connection, opened.capabilities, opened.close); - } - - @override - Future run(Future Function(SqlConnection) action) { - if (_closed) { - throw const OrmException('DRIVER.CLOSED', 'SQLite driver is closed.'); - } - // Queue the whole lease: no caller can enter another caller's transaction. - final result = _tail.then((_) => action(_connection)); - _tail = result.then((_) {}, onError: (Object _, StackTrace _) {}); - return result; - } - - @override - Future close() { - _closed = true; - return _closing ??= _tail.then((_) => _shutdown()); - } -} +export '../src/sqlite/driver.dart' show SqliteDriver; +export '../src/sqlite/failure.dart' show SqliteFailure; +export '../src/sqlite/options.dart' + show SqliteOptions, SqliteWebOptions, SqliteJournal; diff --git a/lib/generate.dart b/lib/generate.dart index 2be108b0..aeee8645 100644 --- a/lib/generate.dart +++ b/lib/generate.dart @@ -1,21 +1,39 @@ -/// Development-time source generation and migration file operations. -/// Application code imports a generated client and a database entrypoint. +/// Source generation, catalog import and reviewed migration files. +/// +/// Use [generateSchema] for an in-memory result or [writeGeneratedSchema] to +/// update the client and physical snapshot. Use [generateQueries] for named SQL. +/// These development tools analyze Dart source; applications import their +/// generated client and a database entrypoint instead. +/// +/// {@category Tooling} +/// {@canonicalFor exception.GenerationException} +/// {@canonicalFor schema.GeneratedSchema} +/// {@canonicalFor schema.generateSchema} +/// {@canonicalFor schema.writeGeneratedSchema} +/// {@canonicalFor queries.GeneratedQueries} +/// {@canonicalFor queries.generateQueries} +/// {@canonicalFor queries.writeGeneratedQueries} +/// {@canonicalFor queries.checkGeneratedQueries} +/// {@canonicalFor queries.checkSqlQueries} +/// {@canonicalFor import.ImportedSchema} +/// {@canonicalFor import.SchemaImportIssue} +/// {@canonicalFor import.importSchema} +/// {@canonicalFor migrations.writeMigrationRegistry} +/// {@canonicalFor migrations.writeMigration} +/// {@canonicalFor migrations.recordMigration} library; -export 'src/generate.dart' +export 'src/generate/exception.dart' show GenerationException; +export 'src/generate/schema.dart' + show GeneratedSchema, generateSchema, writeGeneratedSchema; +export 'src/generate/queries.dart' show - GenerationException, - GeneratedSchema, - generateSchema, - writeGeneratedSchema, GeneratedQueries, generateQueries, writeGeneratedQueries, checkGeneratedQueries, - checkSqlQueries, - ImportedSchema, - SchemaImportIssue, - importSchema, - writeMigrationRegistry, - writeMigration, - recordMigration; + checkSqlQueries; +export 'src/generate/import.dart' + show ImportedSchema, SchemaImportIssue, importSchema; +export 'src/generate/migrations.dart' + show writeMigrationRegistry, writeMigration, recordMigration; diff --git a/lib/mariadb.dart b/lib/mariadb.dart index cc146938..419bc008 100644 --- a/lib/mariadb.dart +++ b/lib/mariadb.dart @@ -1,4 +1,11 @@ -/// Typed MariaDB access with its own capabilities and migration target. +/// Typed MariaDB queries with MariaDB-specific capabilities and history. +/// +/// Await [mariadb] to connect and verify the server before using its generated +/// table getters. Each database owns one queued physical connection. +/// +/// Use `package:orm/drivers/mariadb.dart` when only a raw driver is needed. +/// +/// {@category Databases} library; import 'drivers/mariadb.dart'; @@ -6,6 +13,17 @@ import 'orm.dart'; export 'drivers/mariadb.dart'; export 'orm.dart'; +/// Connects to MariaDB 10.6+ and returns a typed database owning that connection. +/// +/// Completes after authentication, engine/version checks and session setup. +/// MySQL servers are rejected. Migration execution has a stricter MariaDB 11.8+ +/// requirement, independently checked when applying the fixed migration history. +/// TLS certificates are verified unless [options] selects another policy. +/// +/// [onQuery], [onAcquire] and [onDecode] observe runtime operations after opening; +/// they do not include the initial connection handshake. Await [Database.close] +/// when the application owner no longer needs the database. Opening a connection +/// does not generate or apply a schema. Future> mariadb( MariadbOptions options, { void Function(QueryEvent)? onQuery, diff --git a/lib/migrate.dart b/lib/migrate.dart index d2c05c94..bfade302 100644 --- a/lib/migrate.dart +++ b/lib/migrate.dart @@ -1,12 +1,46 @@ -/// Reviewable schema DDL with checksummed transactional and recoverable migrations. +/// Reviewed schema changes and immutable migration histories written in Dart. +/// +/// Build a [Migration] for one database engine, record its checksum in a +/// [MigrationHistory], then apply the validated history with [Migrator]. +/// [SchemaSnapshot] stores physical schema facts without importing live models. +/// +/// See the migration guide in `doc/migrations.md` for creation, review, recovery, +/// and deployment workflows. +/// +/// {@category Migrations} +/// {@canonicalFor backfill.BackfillProgress} +/// {@canonicalFor catalog.CatalogObject} +/// {@canonicalFor catalog.SchemaVerification} +/// {@canonicalFor catalog.TableInfo} +/// {@canonicalFor catalog.inspectTable} +/// {@canonicalFor catalog.verifySchema} +/// {@canonicalFor checks.CheckInfo} +/// {@canonicalFor columns.ColumnInfo} +/// {@canonicalFor columns.inspectColumns} +/// {@canonicalFor columns.verifyColumns} +/// {@canonicalFor diff.SchemaRenames} +/// {@canonicalFor history.MigrationStatus} +/// {@canonicalFor history.Migrator} +/// {@canonicalFor migration.Migration} +/// {@canonicalFor progress.MigrationProgress} +/// {@canonicalFor progress.MigrationStepState} +/// {@canonicalFor schema.createSchema} +/// {@canonicalFor snapshot.SchemaSnapshot} +/// {@canonicalFor source.MigrationHistory} +/// {@canonicalFor source.migrationHistorySource} +/// {@canonicalFor source.migrationSource} +/// {@canonicalFor source.schemaSource} +/// {@canonicalFor step.Backfill} +/// {@canonicalFor step.CheckedSql} +/// {@canonicalFor step.CheckedTableSql} +/// {@canonicalFor step.DropConstraint} +/// {@canonicalFor step.DropTable} +/// {@canonicalFor step.ExecuteSql} +/// {@canonicalFor step.MigrationStep} +/// {@canonicalFor step.RebuildTable} +/// {@canonicalFor validation.validateMigrations} library; -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; - -import 'runtime.dart'; -import 'schema_model.dart'; export 'schema_model.dart' show SqlDialect, @@ -20,40 +54,37 @@ export 'schema_model.dart' ForeignKey, IndexSchema, OrmException; - -part 'src/migrate/schema.dart'; -part 'src/migrate/history.dart'; -part 'src/migrate/snapshot.dart'; -part 'src/migrate/source.dart'; -part 'src/migrate/catalog.dart'; -part 'src/migrate/step.dart'; -part 'src/migrate/diff.dart'; -part 'src/migrate/recovery.dart'; -part 'src/migrate/backfill.dart'; -part 'src/migrate/sqlite_checks.dart'; -part 'src/migrate/checks.dart'; -part 'src/migrate/computed.dart'; -part 'src/migrate/mysql_schema.dart'; -part 'src/migrate/mysql_catalog.dart'; -part 'src/migrate/mysql_diff.dart'; -part 'src/migrate/mysql_recovery.dart'; - -String _quote(String identifier) => '"${identifier.replaceAll('"', '""')}"'; - -Object? _canonical(Object? value) => switch (value) { - Map map => { - for (final key in map.keys.toList()..sort()) key: _canonical(map[key]), - }, - List list => list.map(_canonical).toList(), - _ => value, -}; -String _hash(Object? value) => - sha256.convert(utf8.encode(jsonEncode(_canonical(value)))).toString(); - -Object? _freezeJson(Object? value) => switch (value) { - Map map => Map.unmodifiable({ - for (final entry in map.entries) entry.key: _freezeJson(entry.value), - }), - List list => List.unmodifiable(list.map(_freezeJson)), - _ => value, -}; +export 'src/migrate/backfill.dart' show BackfillProgress; +export 'src/migrate/catalog.dart' + show + CatalogObject, + SchemaVerification, + TableInfo, + inspectTable, + verifySchema; +export 'src/migrate/checks.dart' show CheckInfo; +export 'src/migrate/columns.dart' + show ColumnInfo, inspectColumns, verifyColumns; +export 'src/migrate/diff.dart' show SchemaRenames; +export 'src/migrate/history.dart' show MigrationStatus, Migrator; +export 'src/migrate/migration.dart' show Migration; +export 'src/migrate/progress.dart' show MigrationProgress, MigrationStepState; +export 'src/migrate/schema.dart' show createSchema; +export 'src/migrate/snapshot.dart' show SchemaSnapshot; +export 'src/migrate/source.dart' + show + MigrationHistory, + migrationHistorySource, + migrationSource, + schemaSource; +export 'src/migrate/step.dart' + show + Backfill, + CheckedSql, + CheckedTableSql, + DropConstraint, + DropTable, + ExecuteSql, + MigrationStep, + RebuildTable; +export 'src/migrate/validation.dart' show validateMigrations; diff --git a/lib/migrate_cli.dart b/lib/migrate_cli.dart index 2d854631..806c8245 100644 --- a/lib/migrate_cli.dart +++ b/lib/migrate_cli.dart @@ -1,295 +1,12 @@ -/// Project-owned migration command entrypoint with statically imported history. +/// Migration commands for a project-owned Dart executable. +/// +/// Pass a statically imported history to [runMigrationCli]. Database commands +/// obtain their connection from [MigrationConnection]; inspection requests a +/// read-only connection where the selected database supports it. +/// +/// {@category Tooling} +/// {@canonicalFor migration.MigrationConnection} +/// {@canonicalFor migration.runMigrationCli} library; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'generate.dart'; -import 'migrate.dart'; -import 'runtime.dart'; - -typedef MigrationConnection = FutureOr> Function({ - required bool readOnly, -}); - -/// Runs a project's statically compiled migration history. Offline commands never -/// call [connect]. The connection factory must honor read-only storage requests. -Future runMigrationCli( - List arguments, { - required MigrationHistory history, - required String directory, - SchemaSnapshot? schema, - MigrationConnection? connect, - SchemaRenames renames = const SchemaRenames(), - Map> using = const {}, - bool json = true, -}) async { - void report(Map value) { - if (json) { - stdout.writeln(const JsonEncoder.withIndent(' ').convert(value)); - } else { - for (final entry in value.entries) { - stdout.writeln( - '${entry.key}: ${entry.value is String ? entry.value : jsonEncode(entry.value)}', - ); - } - } - } - - void fail(String message, int code) { - stderr.writeln( - json ? jsonEncode({'error': message, 'exitCode': code}) : message, - ); - exitCode = code; - } - - try { - if (arguments.isEmpty || arguments.singleOrNull == '--help') { - stdout.writeln('''Migration commands: - create [--allow-destructive] - record Record a reviewed, unpublished edit - check - plan - apply [--max-backfill-batches ] - status - verify - baseline - inspect
-Connection, target schema, renames and conversions belong in the Dart entrypoint. -Rebuild static imports with: dart run orm migration registry '''); - return; - } - final command = arguments.first; - final rest = arguments.skip(1).toList(); - final expected = - command == 'create' || command == 'record' || command == 'inspect' - ? 1 - : 0; - var destructive = false; - int? batches; - if (command == 'create' && rest.lastOrNull == '--allow-destructive') { - destructive = true; - rest.removeLast(); - } - if (command == 'apply' && - rest.length == 2 && - rest.first == '--max-backfill-batches') { - batches = int.tryParse(rest.last); - if (batches == null || batches < 1) { - throw const FormatException('Use a positive batch count.'); - } - rest.clear(); - } - if (!{ - 'create', - 'record', - 'check', - 'plan', - 'apply', - 'status', - 'verify', - 'baseline', - 'inspect', - }.contains(command) || - rest.length != expected || - rest.any((value) => value.startsWith('--'))) { - throw const FormatException('Invalid migration command. Use --help.'); - } - if (command == 'record') { - final checksum = await recordMigration( - rest.single, - directory: directory, - history: history, - ); - report({'recorded': rest.single, 'checksum': checksum}); - return; - } - final migrations = history.checked; - if (command == 'check') { - report({ - 'valid': true, - 'dialect': history.dialect.name, - 'migrations': migrations.map((m) => m.id).toList(), - }); - return; - } - if (command == 'create') { - if (schema == null) { - throw const FormatException( - 'Provide the target schema in the Dart entrypoint.', - ); - } - final previous = migrations.lastOrNull; - if (previous != null && previous.snapshot == null) { - throw const FormatException( - 'The previous migration needs a historical schema.', - ); - } - final change = Migration.diff( - rest.single, - dialect: history.dialect, - from: previous?.snapshot ?? SchemaSnapshot([]), - to: schema, - previous: previous?.checksum, - renames: renames, - using: using, - allowDestructive: destructive, - ); - if (change.steps.isEmpty) { - report({'created': null, 'reason': 'Schema is unchanged.'}); - } else { - final path = await writeMigration( - change, - directory: directory, - history: history, - ); - report({'created': path, 'checksum': change.checksum}); - } - return; - } - if (command == 'verify' && schema == null) { - throw const FormatException( - 'Provide the expected schema in the Dart entrypoint.', - ); - } - if (command == 'baseline' && - (migrations.isEmpty || migrations.last.snapshot == null)) { - throw const FormatException( - 'Baseline requires a final historical schema.', - ); - } - if (connect == null) { - throw const FormatException('Provide a database connection factory.'); - } - final db = await connect( - readOnly: !{'apply', 'baseline'}.contains(command), - ); - try { - if (db.dialect != history.dialect) { - throw OrmException( - 'MIGRATION.TARGET', - 'History targets ${history.dialect.name}, connection is ${db.dialect.name}.', - ); - } - final migrator = Migrator(db); - switch (command) { - case 'plan': - final pending = await migrator.plan(migrations); - report({ - 'atomic': !pending.any( - (m) => m.steps.any( - (s) => s is CheckedSql || s is CheckedTableSql || s is Backfill, - ), - ), - 'pending': [ - for (final m in pending) - { - 'id': m.id, - 'checksum': m.checksum, - 'steps': m.steps.map((s) => s.toJson()).toList(), - }, - ], - 'progress': (await migrator.progress()) - .map((p) => p.toJson()) - .toList(), - }); - case 'apply': - final applied = await migrator.apply( - migrations, - maxBackfillBatches: batches, - ); - report({ - 'applied': applied, - if (batches != null) - 'complete': (await migrator.plan(migrations)).isEmpty, - }); - case 'status': - report({ - 'applied': [ - for (final m in await migrator.history()) - {'id': m.id, 'checksum': m.checksum}, - ], - 'progress': (await migrator.progress()) - .map((p) => p.toJson()) - .toList(), - }); - case 'baseline' || 'verify': - final result = command == 'baseline' - ? await migrator.baseline( - migrations, - expected: migrations.last.snapshot!, - ) - : await verifySchema(db, schema!); - report({ - 'matches': result.matches, - 'differences': result.differences, - 'unmanaged': [ - for (final o in result.unmanaged) - {'kind': o.kind, 'name': o.name, 'definition': o.definition}, - ], - }); - if (!result.matches) exitCode = 2; - case 'inspect': - final table = await inspectTable(db, rest.single); - report({ - 'table': table.name, - 'columns': [ - for (final c in table.columns) - { - 'name': c.name, - 'storageType': c.storageType, - 'nullable': c.nullable, - 'default': c.defaultSql, - 'generated': c.generated, - if (c.computed case final computed?) - 'computed': { - 'expression': computed.expression(db.dialect), - 'storage': computed.storage.name, - }, - if (c.integerBits != null) 'integerBits': c.integerBits, - if (c.decimalPrecision != null) - 'decimalPrecision': c.decimalPrecision, - if (c.decimalPrecision != null) - 'decimalScale': c.decimalScale ?? 0, - if (c.temporalPrecision != null) - 'temporalPrecision': c.temporalPrecision, - if (c.collation != null) 'collation': c.collation, - }, - ], - 'primaryKey': table.primaryKey, - 'uniqueKeys': table.uniqueKeys, - 'foreignKeys': [ - for (final k in table.foreignKeys) - { - 'columns': k.columns, - 'target': k.target, - 'targetColumns': k.targetColumns, - 'onDelete': k.onDelete, - }, - ], - 'indexes': [ - for (final i in table.indexes) - {'name': i.name, 'columns': i.columns, 'unique': i.unique}, - ], - 'checks': [ - for (final check in table.checks) - {'name': check.name, 'expression': check.expression}, - ], - 'unmanaged': [ - for (final o in table.unmanaged) - {'kind': o.kind, 'name': o.name, 'definition': o.definition}, - ], - }); - } - } finally { - await db.close(); - } - } on FormatException catch (error) { - fail(error.message, 64); - } on ArgumentError catch (_) { - fail('Invalid migration argument. Use --help.', 64); - } catch (error) { - fail(error.toString(), 1); - } -} +export 'src/cli/migration.dart' show MigrationConnection, runMigrationCli; diff --git a/lib/mysql.dart b/lib/mysql.dart index cb6d0ebf..df20b3cd 100644 --- a/lib/mysql.dart +++ b/lib/mysql.dart @@ -1,4 +1,12 @@ -/// Typed MySQL access. Use drivers/mysql.dart for the independent raw driver. +/// Typed MySQL queries with explicit connection and transaction ownership. +/// +/// Await [mysql] to connect and verify the server before using its generated +/// table getters. Each database owns one queued physical connection. MySQL and +/// MariaDB have distinct engine identities and migration histories. +/// +/// Use `package:orm/drivers/mysql.dart` when only a raw driver is needed. +/// +/// {@category Databases} library; import 'drivers/mysql.dart'; @@ -6,6 +14,16 @@ import 'orm.dart'; export 'drivers/mysql.dart'; export 'orm.dart'; +/// Connects to MySQL 8.4+ and returns a typed database owning that connection. +/// +/// Completes after authentication, engine/version checks and session setup. +/// MariaDB servers are rejected; use their separate entrypoint. TLS certificates +/// are verified unless [options] explicitly selects another policy. +/// +/// [onQuery], [onAcquire] and [onDecode] observe runtime operations after opening; +/// they do not include the initial connection handshake. Await [Database.close] +/// when the application owner no longer needs the database. Opening a connection +/// does not generate or apply a schema. Future> mysql( MysqlOptions options, { void Function(QueryEvent)? onQuery, diff --git a/lib/orm.dart b/lib/orm.dart index 4164fb71..cb061e52 100644 --- a/lib/orm.dart +++ b/lib/orm.dart @@ -1,16 +1,21 @@ -/// Typed relational queries and explicit database sessions. +/// Typed queries, explicit transactions and change subscriptions. +/// +/// Import a database entrypoint such as `package:orm/sqlite.dart` to open a +/// connection and use this API. [Database] also accepts an independently created +/// driver. Generated clients add typed table getters to each database/session. +/// +/// Use [SqlBuilder] to compile queries without connecting, and [SqlDatabase] for +/// raw SQL execution. Schema declarations and migration tools have separate +/// entrypoints so application code need not import development tooling. +/// +/// {@category Databases} +/// {@canonicalFor database.Database} +/// {@canonicalFor observation.DecodeEvent} +/// {@canonicalFor watch.WatchQuery} library; -import 'dart:async'; - -import 'runtime.dart'; -import 'sql.dart'; export 'sql.dart'; -export 'values.dart'; -export 'driver.dart'; export 'runtime.dart'; -export 'schema_model.dart'; - -part 'src/database.dart'; -part 'src/observation.dart'; -part 'src/watch.dart'; +export 'src/orm/database.dart' show Database; +export 'src/orm/observation.dart' show DecodeEvent; +export 'src/orm/watch.dart' show WatchQuery; diff --git a/lib/postgres.dart b/lib/postgres.dart index 79764da8..b7c107dc 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -1,4 +1,12 @@ -/// PostgreSQL ORM convenience entrypoint. Raw SQL drivers are in drivers/postgres.dart. +/// Typed PostgreSQL queries backed by an owned connection pool. +/// +/// [postgres] validates configuration and returns immediately. Physical +/// connections open when an operation acquires a pool lease. Import a generated +/// client to access model-specific table getters. +/// +/// Use `package:orm/drivers/postgres.dart` for a raw driver or a borrowed pool. +/// +/// {@category Databases} library; import 'orm.dart'; @@ -6,6 +14,16 @@ import 'drivers/postgres.dart'; export 'orm.dart'; export 'drivers/postgres.dart'; +/// Creates a typed database that owns a lazily connected PostgreSQL pool. +/// +/// Returns synchronously after validating [options]. A successful return does +/// not prove the server is reachable: authentication, TLS and network failures +/// are reported when an operation first acquires a connection. Execute an +/// explicit query if application startup must verify connectivity. +/// +/// [onAcquire] includes pool lease acquisition; [onQuery] and [onDecode] observe +/// runtime SQL and result decoding. Await [Database.close] to close the owned +/// pool. Creating the database does not generate or apply a schema. Database postgres( PostgresOptions options, { void Function(QueryEvent)? onQuery, diff --git a/lib/runtime.dart b/lib/runtime.dart index 9fb2b17a..52abda11 100644 --- a/lib/runtime.dart +++ b/lib/runtime.dart @@ -1,48 +1,36 @@ -/// Raw SQL sessions, transactions and cursor lifetimes without a query builder. +/// Connection leases, transactions and streaming for parameterized SQL. +/// +/// Construct [SqlDatabase] with a driver when an application needs raw SQL +/// without typed queries. The database owns the driver; session and transaction +/// callbacks borrow connections and must not escape their callback lifetimes. +/// +/// {@category Execution} +/// {@canonicalFor database.SqlDatabase} +/// {@canonicalFor database.SqlDatabaseStreaming} +/// {@canonicalFor events.QueryOperation} +/// {@canonicalFor events.QueryEvent} +/// {@canonicalFor events.AcquisitionEvent} +/// {@canonicalFor options.TransactionOptions} +/// {@canonicalFor options.Isolation} +/// {@canonicalFor options.PostgresTransaction} +/// {@canonicalFor options.SqliteTransactionMode} +/// {@canonicalFor options.SqliteTransaction} +/// {@canonicalFor options.MysqlTransaction} +/// {@canonicalFor options.MariadbTransaction} +/// {@canonicalFor transaction.TransactionRetry} library; -import 'dart:async'; -import 'dart:math' as math; - -import 'driver.dart'; export 'driver.dart'; - -part 'src/runtime/database.dart'; -part 'src/runtime/session_connection.dart'; -part 'src/runtime/mysql_transaction.dart'; -part 'src/runtime/options.dart'; -part 'src/runtime/stream.dart'; -part 'src/execution.dart'; -part 'src/transaction.dart'; - -enum QueryOperation { execute, cursorOpen, cursorFetch, cursorClose } - -final class QueryEvent { - final QueryOperation operation; - final String sql; - final int parameterCount; - final Duration elapsed; - final int? rowCount; - final Object? error; - const QueryEvent({ - this.operation = QueryOperation.execute, - required this.sql, - required this.parameterCount, - required this.elapsed, - this.rowCount, - this.error, - }); -} - -/// Time until a driver lease is granted or acquisition fails. Includes native -/// pool wait/connection setup; it does not separate those driver internals. -final class AcquisitionEvent { - final Duration elapsed; - final bool reusedConnection; - final Object? error; - const AcquisitionEvent({ - required this.elapsed, - required this.reusedConnection, - this.error, - }); -} +export 'src/runtime/database.dart' show SqlDatabase, SqlDatabaseStreaming; +export 'src/runtime/events.dart' + show QueryOperation, QueryEvent, AcquisitionEvent; +export 'src/runtime/options.dart' + show + TransactionOptions, + Isolation, + PostgresTransaction, + SqliteTransactionMode, + SqliteTransaction, + MysqlTransaction, + MariadbTransaction; +export 'src/runtime/transaction.dart' show TransactionRetry; diff --git a/lib/schema.dart b/lib/schema.dart index 00f4d0a0..e23dde65 100644 --- a/lib/schema.dart +++ b/lib/schema.dart @@ -1,144 +1,62 @@ -/// Compile-time schema declarations. Key selectors are analyzed as source and -/// never called with synthetic row values. +/// Declares typed Dart models, storage annotations, and relationships. +/// +/// Import this library in a schema source file. [entity] associates a Dart model +/// with a physical table; annotations such as [Id], [Default], and [UseCodec] +/// define stored values. Top-level key and relationship declarations generate +/// query access without executing application constructors or opening a database. +/// +/// ```dart +/// import 'package:orm/schema.dart'; +/// +/// final class User(@Id.generated() final int id, final String email); +/// final users = entity(); +/// final uniqueEmail = users.unique((user) => user.email); +/// ``` +/// +/// Run `dart run orm generate path/to/schema.dart` to generate the typed client. +/// Use `package:orm/schema_model.dart` for physical metadata and +/// `package:orm/migrate.dart` to apply reviewed schema changes. +/// +/// {@category Declarations} +/// {@canonicalFor declaration.ClientDefault} +/// {@canonicalFor declaration.ColumnName} +/// {@canonicalFor declaration.Computed} +/// {@canonicalFor declaration.DecimalDigits} +/// {@canonicalFor declaration.Default} +/// {@canonicalFor declaration.Entity} +/// {@canonicalFor declaration.EntityKey} +/// {@canonicalFor declaration.EnumValue} +/// {@canonicalFor declaration.Id} +/// {@canonicalFor declaration.IntegerBits} +/// {@canonicalFor declaration.ReferentialAction} +/// {@canonicalFor declaration.SchemaConstraint} +/// {@canonicalFor declaration.SqlDeclaration} +/// {@canonicalFor declaration.TemporalPrecision} +/// {@canonicalFor declaration.Unique} +/// {@canonicalFor declaration.UseCodec} +/// {@canonicalFor declaration.entity} +/// {@canonicalFor declaration.sqlQuery} library; -import 'schema_model.dart' show Codec, ComputedStorage; -export 'schema_model.dart'; - -/// A public const codec reference. Generation reads its type and storage tag; -/// it never executes the application's encode/decode functions. -final class UseCodec { - final Codec codec; - const UseCodec(this.codec); -} - -/// Stable database text for an enum constant, independent of its Dart name. -final class EnumValue { - final String value; - const EnumValue(this.value); -} - -final class Id { - final bool generated; - const Id() : generated = false; - const Id.generated() : generated = true; -} - -final class Unique { - const Unique(); -} - -final class ColumnName { - final String name; - const ColumnName(this.name); -} - -final class Default { - final String expression; - const Default.sql(this.expression); -} - -/// Generates an omitted insert value in Dart. This never declares SQL DEFAULT. -/// The factory is referenced during generation, not invoked. -final class ClientDefault { - final T Function() factory; - const ClientDefault(this.factory); -} - -/// SQL computed by the database; omitted from generated writes. -final class Computed { - final String expression; - final String? sqlite, postgres, mysql, mariadb; - final ComputedStorage storage; - const Computed.sql( - this.expression, { - this.sqlite, - this.postgres, - this.mysql, - this.mariadb, - this.storage = ComputedStorage.stored, - }); -} - -/// Signed integer column storage. SQL expression results retain the int codec. -final class IntegerBits { - final int value; - const IntegerBits(this.value); -} - -/// Decimal column precision and scale, matching PostgreSQL NUMERIC(p, s). -final class DecimalDigits { - final int precision; - final int scale; - const DecimalDigits(this.precision, [this.scale = 0]); -} - -/// Fractional second digits (0..6) for time, local timestamp or UTC instant columns. -final class TemporalPrecision { - final int digits; - const TemporalPrecision(this.digits); -} - -enum ReferentialAction { restrict, cascade, setNull, setDefault, noAction } - -/// A fixed SQL file with declared result and parameter Record types. -/// Run query generation, then check the manifest against each target database. -SqlDeclaration sqlQuery({ - String? sqlite, - String? postgres, - String? mysql, - String? mariadb, -}) => SqlDeclaration(sqlite, postgres, mysql, mariadb); - -final class SqlDeclaration { - final String? sqlite, postgres, mysql, mariadb; - const SqlDeclaration(this.sqlite, this.postgres, this.mysql, this.mariadb); -} - -Entity entity({String? table}) => Entity(table); - -final class Entity { - final String? table; - const Entity(this.table); - EntityKey key(K Function(M) selector) => EntityKey(this, selector); - SchemaConstraint primaryKey(K Function(M) selector) => - const SchemaConstraint(); - SchemaConstraint unique(K Function(M) selector) => - const SchemaConstraint(); - - /// Trusted SQL using physical column names. Omitted name uses the declaration - /// name; explicit name: null preserves an unnamed imported constraint. - SchemaConstraint check( - String expression, { - String? name, - String? sqlite, - String? postgres, - String? mysql, - String? mariadb, - }) => const SchemaConstraint(); - SchemaConstraint index( - K Function(M) selector, { - String? name, - bool unique = false, - }) => const SchemaConstraint(); -} - -final class EntityKey { - final Entity entity; - final K Function(M) selector; - const EntityKey(this.entity, this.selector); - SchemaConstraint references( - EntityKey target, { - String? inverse, - ReferentialAction onDelete = ReferentialAction.restrict, - }) => const SchemaConstraint(); - - /// Read-only query navigation without a database foreign key. Matching rows - /// may be absent or duplicated; this declaration creates no write effects. - SchemaConstraint relatesTo(EntityKey target, {String? inverse}) => - const SchemaConstraint(); -} - -final class SchemaConstraint { - const SchemaConstraint(); -} +export 'values.dart'; +export 'schema_model.dart' show ComputedStorage; +export 'src/schema/declaration.dart' + show + ClientDefault, + ColumnName, + Computed, + DecimalDigits, + Default, + Entity, + EntityKey, + EnumValue, + Id, + IntegerBits, + ReferentialAction, + SchemaConstraint, + SqlDeclaration, + TemporalPrecision, + Unique, + UseCodec, + entity, + sqlQuery; diff --git a/lib/schema_model.dart b/lib/schema_model.dart index da512345..a3ec61b5 100644 --- a/lib/schema_model.dart +++ b/lib/schema_model.dart @@ -1,180 +1,33 @@ -/// Physical table metadata shared by SQL compilation and schema migrations. +/// Physical table, column, key, index, and constraint metadata. +/// +/// [TableSchema] and [Column] describe storage independently of Dart row models, +/// generated clients, and live connections. They can be assembled directly for +/// schema tooling or consumed from generated Dart snapshots. +/// +/// Collection inputs to [TableSchema] are copied into immutable lists. Creating +/// metadata does not apply DDL: migration tooling validates the target engine and +/// executes reviewed schema changes separately. +/// +/// Use `package:orm/schema.dart` for application model annotations. +/// +/// {@category Schema} +/// {@canonicalFor model.CheckSchema} +/// {@canonicalFor model.Column} +/// {@canonicalFor model.ComputedColumn} +/// {@canonicalFor model.ComputedStorage} +/// {@canonicalFor model.ForeignKey} +/// {@canonicalFor model.IndexSchema} +/// {@canonicalFor model.TableSchema} library; -import 'driver.dart' show SqlDialect; -import 'values.dart'; -export 'driver.dart' show SqlDialect; export 'values.dart'; - -final class Column { - final String name; - final Codec codec; - final bool nullable; - final bool generated; - final String? defaultSql; - final ComputedColumn? computed; - - /// Called once for an omitted value when constructing an insert. Prepared - /// mutations retain that value; compilation and updates never call this. - final T Function()? clientDefault; - - /// Signed integer storage width (16, 32 or 64). The default is 64. - /// This describes the column, not the result width of SQL arithmetic. - final int? integerBits; - final int? decimalPrecision; - final int? decimalScale; - final int? temporalPrecision; - const Column( - this.name, - this.codec, { - this.nullable = false, - this.generated = false, - this.defaultSql, - this.computed, - this.clientDefault, - this.integerBits, - this.decimalPrecision, - this.decimalScale, - this.temporalPrecision, - }); -} - -enum ComputedStorage { stored, virtual } - -/// Database-computed SQL using physical column names. -final class ComputedColumn { - final String sqlite, postgres; - final String? mysql, mariadb; - final ComputedStorage storage; - const ComputedColumn( - String expression, { - this.storage = ComputedStorage.stored, - }) : sqlite = expression, - postgres = expression, - mysql = expression, - mariadb = expression; - const ComputedColumn.forDialects({ - required this.sqlite, - required this.postgres, - this.mysql, - this.mariadb, - this.storage = ComputedStorage.stored, - }); - String expression(SqlDialect dialect) => switch (dialect) { - SqlDialect.sqlite => sqlite, - SqlDialect.postgres => postgres, - SqlDialect.mysql => - mysql ?? - (throw const OrmException( - 'SCHEMA.DIALECT', - 'Declare the MySQL expression explicitly.', - )), - SqlDialect.mariadb => - mariadb ?? - (throw const OrmException( - 'SCHEMA.DIALECT', - 'Declare the MariaDB expression explicitly.', - )), - }; -} - -final class ForeignKey { - final List columns; - final String target; - final List targetColumns; - final String onDelete; - const ForeignKey( - this.columns, - this.target, - this.targetColumns, { - this.onDelete = 'RESTRICT', - }); -} - -/// A row CHECK expression. A null name leaves naming to the database. -final class CheckSchema { - final String? name; - final String sqlite; - final String postgres; - final String? mysql, mariadb; - const CheckSchema(this.name, String expression) - : sqlite = expression, - postgres = expression, - mysql = expression, - mariadb = expression; - const CheckSchema.forDialects( - this.name, { - required this.sqlite, - required this.postgres, - this.mysql, - this.mariadb, - }); - String expression(SqlDialect dialect) => switch (dialect) { - SqlDialect.sqlite => sqlite, - SqlDialect.postgres => postgres, - SqlDialect.mysql => - mysql ?? - (throw const OrmException( - 'SCHEMA.DIALECT', - 'Declare the MySQL expression explicitly.', - )), - SqlDialect.mariadb => - mariadb ?? - (throw const OrmException( - 'SCHEMA.DIALECT', - 'Declare the MariaDB expression explicitly.', - )), - }; -} - -final class IndexSchema { - final String name; - final List columns; - final bool unique; - const IndexSchema(this.name, this.columns, {this.unique = false}); -} - -final class TableSchema { - final String name; - final List> columns; - final List primaryKey; - final List> uniqueKeys; - final List foreignKeys; - final List indexes; - final List checks; - final List> clientDefaults; - TableSchema( - this.name, { - required List> columns, - List primaryKey = const [], - List> uniqueKeys = const [], - List foreignKeys = const [], - List indexes = const [], - List checks = const [], - }) : columns = List.unmodifiable(columns), - clientDefaults = List.unmodifiable( - columns.where((c) => c.clientDefault != null), - ), - primaryKey = List.unmodifiable(primaryKey), - uniqueKeys = List.unmodifiable( - uniqueKeys.map(List.unmodifiable), - ), - foreignKeys = List.unmodifiable([ - for (final key in foreignKeys) - ForeignKey( - List.unmodifiable(key.columns), - key.target, - List.unmodifiable(key.targetColumns), - onDelete: key.onDelete, - ), - ]), - checks = List.unmodifiable(checks), - indexes = List.unmodifiable([ - for (final index in indexes) - IndexSchema( - index.name, - List.unmodifiable(index.columns), - unique: index.unique, - ), - ]); -} +export 'driver.dart' show SqlDialect; +export 'src/schema/model.dart' + show + CheckSchema, + Column, + ComputedColumn, + ComputedStorage, + ForeignKey, + IndexSchema, + TableSchema; diff --git a/lib/sql.dart b/lib/sql.dart index bd8e6bc5..f6e9aec9 100644 --- a/lib/sql.dart +++ b/lib/sql.dart @@ -1,32 +1,117 @@ -/// Typed SQL construction and result shaping, independent of connection runtimes. +/// Typed SQL construction, projections and query inspection. +/// +/// [SqlBuilder] compiles a query without opening a connection. Select a scalar, +/// a typed Record with `.row`, or map selected values into an application model. +/// Executing a query requires binding it to an ORM database session. +/// +/// {@category Queries} +/// {@canonicalFor context.QueryContext} +/// {@canonicalFor context.SqlBuilder} +/// {@canonicalFor expression.Expr} +/// {@canonicalFor expression.value} +/// {@canonicalFor expression.sql} +/// {@canonicalFor expression.Predicate} +/// {@canonicalFor expression.TextExpression} +/// {@canonicalFor expression.NumericExpression} +/// {@canonicalFor expression.OrderTerm} +/// {@canonicalFor expression.TimeExpression} +/// {@canonicalFor expression.LocalDateTimeExpression} +/// {@canonicalFor expression.InstantExpression} +/// {@canonicalFor expression.DecimalExpression} +/// {@canonicalFor selection.Selection} +/// {@canonicalFor selection.Selection2} +/// {@canonicalFor selection.Selection3} +/// {@canonicalFor selection.Selection4} +/// {@canonicalFor selection.Selection5} +/// {@canonicalFor selection.Selection6} +/// {@canonicalFor selection.fields} +/// {@canonicalFor table.TableRef} +/// {@canonicalFor table.Fields} +/// {@canonicalFor table.ReadField} +/// {@canonicalFor table.Field} +/// {@canonicalFor table.NumericField} +/// {@canonicalFor table.Table} +/// {@canonicalFor table.Change} +/// {@canonicalFor table.ChangeField} +/// {@canonicalFor query.Query} +/// {@canonicalFor query.TableSet} +/// {@canonicalFor plan.PlannedColumn} +/// {@canonicalFor plan.PlannedJoin} +/// {@canonicalFor plan.QueryPlan} +/// {@canonicalFor plan.RelationLoadPlan} +/// {@canonicalFor mutation.Assignment} +/// {@canonicalFor mutation.Mutation} +/// {@canonicalFor mutation.Returning} +/// {@canonicalFor relation.ToOneStrategy} +/// {@canonicalFor relation.Relation} +/// {@canonicalFor batch.BatchInsert} +/// {@canonicalFor batch.BatchReturning} +/// {@canonicalFor joins.TableAlias} +/// {@canonicalFor joins.WindowFrame} +/// {@canonicalFor joins.rowNumber} +/// {@canonicalFor joins.rank} +/// {@canonicalFor cte.Cte} +/// {@canonicalFor cte.CteFields} +/// {@canonicalFor cursor.NullOrder} +/// {@canonicalFor cursor.CursorTerm} +/// {@canonicalFor cursor.KeysetQuery} +/// {@canonicalFor union.SqlRow2} +/// {@canonicalFor union.SqlRow3} +/// {@canonicalFor union.SqlRow4} +/// {@canonicalFor union.SqlRow5} +/// {@canonicalFor union.SqlRow6} +/// {@canonicalFor union.SetQueries} +/// {@canonicalFor union.UnionFields} +/// {@canonicalFor stream.QueryStreaming} +/// {@canonicalFor named_sql.SqlTemplate} +/// {@canonicalFor named_sql.SqlQueryDefinition} library; -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'driver.dart'; -import 'schema_model.dart'; export 'driver.dart'; export 'schema_model.dart'; - -part 'src/sql_context.dart'; -part 'src/temporal_sql.dart'; -part 'src/decimal_sql.dart'; -part 'src/decimal_division.dart'; -part 'src/decimal_average.dart'; -part 'src/sql.dart'; -part 'src/named_sql.dart'; -part 'src/selection.dart'; -part 'src/table.dart'; -part 'src/query.dart'; -part 'src/plan.dart'; -part 'src/mutation.dart'; -part 'src/relation.dart'; -part 'src/batch.dart'; -part 'src/advanced.dart'; -part 'src/cte.dart'; -part 'src/cursor.dart'; -part 'src/union.dart'; -part 'src/query_reads.dart'; -part 'src/query_stream.dart'; +export 'src/query/context.dart' show QueryContext, SqlBuilder; +export 'src/query/expression.dart' + show + Expr, + value, + sql, + Predicate, + TextExpression, + NumericExpression, + OrderTerm, + TimeExpression, + LocalDateTimeExpression, + InstantExpression, + DecimalExpression; +export 'src/query/selection.dart' + show + Selection, + Selection2, + Selection3, + Selection4, + Selection5, + Selection6, + fields; +export 'src/query/table.dart' + show + TableRef, + Fields, + ReadField, + Field, + NumericField, + Table, + Change, + ChangeField; +export 'src/query/query.dart' show Query, TableSet; +export 'src/query/plan.dart' + show PlannedColumn, PlannedJoin, QueryPlan, RelationLoadPlan; +export 'src/query/mutation.dart' show Assignment, Mutation, Returning; +export 'src/query/relation.dart' show ToOneStrategy, Relation; +export 'src/query/batch.dart' show BatchInsert, BatchReturning; +export 'src/query/joins.dart' show TableAlias, WindowFrame, rowNumber, rank; +export 'src/query/cte.dart' show Cte, CteFields; +export 'src/query/cursor.dart' show NullOrder, CursorTerm, KeysetQuery; +export 'src/query/union.dart' + show SqlRow2, SqlRow3, SqlRow4, SqlRow5, SqlRow6, SetQueries, UnionFields; +export 'src/query/stream.dart' show QueryStreaming; +export 'src/query/named_sql.dart' show SqlTemplate, SqlQueryDefinition; diff --git a/lib/sqlite.dart b/lib/sqlite.dart index b1ee7037..8ec1a59e 100644 --- a/lib/sqlite.dart +++ b/lib/sqlite.dart @@ -1,4 +1,13 @@ -/// SQLite ORM convenience entrypoint. Raw SQL drivers are in drivers/sqlite.dart. +/// Typed SQLite queries on native Dart, Flutter and the web. +/// +/// Open a temporary database with [SqliteOptions.memory], a native file with +/// [SqliteOptions.file], or native/browser persistence with +/// [SqliteOptions.persistent]. The returned database owns its background worker. +/// Import a generated client to access model-specific table getters. +/// +/// Use `package:orm/drivers/sqlite.dart` when only a raw driver is needed. +/// +/// {@category Databases} library; import 'orm.dart'; @@ -6,6 +15,16 @@ import 'drivers/sqlite.dart'; export 'orm.dart'; export 'drivers/sqlite.dart'; +/// Opens SQLite and returns a typed database that owns its driver and worker. +/// +/// Completes after SQLite and the requested storage have initialized. Native +/// applications supply their own file path; persistent browser databases use an +/// OPFS name. Memory databases are discarded when the database closes. +/// +/// [onQuery], [onAcquire] and [onDecode] observe runtime execution and result +/// decoding. Opening the database does not generate or apply a schema. Apply the +/// reviewed migration history before querying, and await [Database.close] when +/// the application owner no longer needs the connection. Future> sqlite( SqliteOptions options, { void Function(QueryEvent)? onQuery, diff --git a/lib/sqlite_web_worker.dart b/lib/sqlite_web_worker.dart index 7d235977..fa0a5b50 100644 --- a/lib/sqlite_web_worker.dart +++ b/lib/sqlite_web_worker.dart @@ -1,172 +1,10 @@ -/// Compile a worker entry point whose main calls [runSqliteWebWorker]. +/// Entry for compiling a custom dedicated SQLite Web worker. +/// +/// Ordinary Dart and Flutter applications use the packaged worker automatically. +/// Custom worker entrypoints call [runSqliteWebWorker] once from `main`. +/// +/// {@category Drivers} +/// {@canonicalFor web_worker.runSqliteWebWorker} library; -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:typed_data'; - -import 'package:sqlite3/wasm.dart' as sqlite; -import 'package:web/web.dart' as web; - -import 'driver.dart'; -import 'src/sqlite/execution.dart'; -import 'src/sqlite/web_build.dart'; -import 'src/sqlite/web_wire.dart'; - -void runSqliteWebWorker() { - final self = globalContext as web.DedicatedWorkerGlobalScope; - final server = _WorkerDatabase(); - Future tail = Future.value(); - self.onmessage = ((web.MessageEvent event) { - tail = tail.then((_) async { - final message = event.data! as JSArray; - final id = message[0]; - try { - final result = await server.handle( - (message[1]! as JSString).toDart, - message[2], - ); - self.postMessage([id, true.toJS, server.active?.toJS, result].toJS); - } catch (error) { - final JSArray details; - if (error is sqlite.SqliteException) { - details = [ - 'sqlite'.toJS, - error.message.toJS, - error.resultCode.toJS, - error.extendedResultCode.toJS, - ].toJS; - } else { - details = [ - (error is OrmException ? error.code : 'DRIVER.SQLITE').toJS, - error.toString().toJS, - ].toJS; - } - self.postMessage([id, false.toJS, server.active?.toJS, details].toJS); - } - }); - }).toJS; -} - -final class _WorkerDatabase { - sqlite.WasmSqlite3? engine; - sqlite.CommonDatabase? db; - sqlite.SimpleOpfsFileSystem? opfs; - sqlite.InMemoryFileSystem? memory; - SqliteExecutor? executor; - bool? get active => db == null ? null : !db!.autocommit; - - Future handle(String operation, JSAny? payload) async { - if (operation == 'hello') { - return [sqliteWebProtocol.toJS, sqliteWorkerBuild.toJS].toJS; - } - if (operation == 'open') { - if (engine != null) { - throw const OrmException('DRIVER.OPEN', 'Worker is already open.'); - } - final options = payload! as JSArray; - final wasm = (options[0]! as JSString).toDart; - final storage = (options[1]! as JSString).toDart; - try { - final web.Response response; - try { - response = await (globalContext as web.DedicatedWorkerGlobalScope) - .fetch( - wasm.toJS, - web.RequestInit(integrity: (options[3]! as JSString).toDart), - ) - .toDart; - if (!response.ok) throw StateError('HTTP ${response.status}'); - } catch (e) { - throw OrmException( - 'DRIVER.ASSET', - 'SQLite WASM fetch or integrity check failed.', - cause: e, - ); - } - final bytes = (await response.arrayBuffer().toDart).toDart; - final sqlite3 = engine = await sqlite.WasmSqlite3.load( - Uint8List.view(bytes), - ); - if (storage == 'opfs') { - opfs = await sqlite.SimpleOpfsFileSystem.loadFromStorage( - 'dart-orm/${(options[2]! as JSString).toDart}', - ); - sqlite3.registerVirtualFileSystem(opfs!, makeDefault: true); - } else if (storage == 'memory') { - memory = sqlite.InMemoryFileSystem(); - sqlite3.registerVirtualFileSystem(memory!, makeDefault: true); - } else { - throw const OrmException( - 'DRIVER.STORAGE', - 'Unknown browser storage.', - ); - } - final opened = db = sqlite3.open( - storage == 'memory' ? ':memory:' : '/database', - ); - executor = SqliteExecutor(opened); - // Sync OPFS handles own the database exclusively; a busy wait cannot - // coordinate another owner. Keep durable rollback-journal storage. - final maxParameters = configureSqlite( - opened, - journal: storage == 'memory' ? 'memory' : 'delete', - busyTimeout: Duration.zero, - fullSync: true, - ); - return [sqlite3.version.versionNumber.toJS, maxParameters.toJS].toJS; - } catch (_) { - close(); - rethrow; - } - } - final opened = db; - if (opened == null) { - throw const OrmException('DRIVER.CLOSED', 'SQLite worker is not open.'); - } - switch (operation) { - case 'execute': - return sqliteWebResult( - executor!.execute(sqliteWebDartCommand(payload)), - ); - case 'cursor': - return executor!.openCursor(sqliteWebDartCommand(payload)).toJS; - case 'fetch': - final parts = payload! as JSArray; - return sqliteWebResult( - executor!.fetch( - (parts[0]! as JSNumber).toDartInt, - (parts[1]! as JSNumber).toDartInt, - ), - ); - case 'release': - executor!.release((payload! as JSNumber).toDartInt); - return null; - case 'close': - close(); - return null; - default: - throw const OrmException( - 'DRIVER.PROTOCOL', - 'Unknown SQLite worker command.', - ); - } - } - - void close() { - if (executor != null) { - executor!.close(); - } else { - db?.close(); - } - executor = null; - db = null; - if (opfs case final fs?) { - engine?.unregisterVirtualFileSystem(fs); - fs.close(); - } - if (memory case final fs?) engine?.unregisterVirtualFileSystem(fs); - opfs = null; - memory = null; - } -} +export 'src/sqlite/web_worker.dart' show runSqliteWebWorker; diff --git a/lib/src/advanced.dart b/lib/src/advanced.dart deleted file mode 100644 index 14fee654..00000000 --- a/lib/src/advanced.dart +++ /dev/null @@ -1,359 +0,0 @@ -part of '../sql.dart'; - -/// Explicit aliases support self joins and any number of joins without adding -/// a growing number of generic join-result types. -final class TableAlias { - final Table definition; - final F fields; - final _CteDefinition? _cte; - late final String _marker = _presenceName(); - TableAlias._(this.definition, [this._cte]) - : fields = definition.createFields(TableRef(definition.schema)); - String _presenceName() { - var name = '_orm_present'; - while (definition.schema.columns.any((c) => c.name.toLowerCase() == name)) { - name += '_'; - } - return name; - } - - Selection optional(Selection selection) => - _OptionalJoin(this, selection); - Selection get row => optional(definition.selectRow(fields)); - Expr nullable(Expr expression) => - Expr._(expression._node, expression.codec.nullable()); - Expr get isPresent => - Expr._(_Presence(this), Codecs.integer.nullable()).isNotNull(); -} - -final class _Join( - final TableAlias alias, - final Expr on, - final bool left, -); - -final class _Presence(final TableAlias alias) extends _Node { - @override - String writeSql(_Writer w) { - if (!w.leftJoins.contains(alias.fields.table)) { - throw const OrmException( - 'QUERY.OUTER_JOIN', - 'Optional projections require this alias to be left joined.', - ); - } - return '${w.quote(w.aliases[alias.fields.table]!)}.${w.quote(alias._marker)}'; - } -} - -final class _OptionalJoin( - final TableAlias alias, - final Selection selection, -) extends Selection { - @override - _Decoder _bind(_SelectionPlan plan) { - final marker = plan.column( - Expr._(_Presence(alias), Codecs.integer.nullable()), - ); - final decode = plan.optional(alias.fields.table, selection); - return (row) => row[marker] == null ? null : decode(row); - } -} - -final class _Subquery( - final Query query, { - final bool exists = false, -}) extends _Node { - @override - String writeSql(_Writer w) { - final (plan, _) = query._plan(); - if (plan.relations.isNotEmpty) { - throw const OrmException( - 'QUERY.SUBQUERY', - 'SQL subqueries cannot include batch-loaded relations.', - ); - } - if (w.dialect != query.database.dialect) { - throw const OrmException( - 'QUERY.DIALECT', - 'Subqueries must use the enclosing SQL dialect.', - ); - } - return '${exists ? 'EXISTS ' : ''}(${query._write(w, plan)})'; - } -} - -enum WindowFrame { rowsToCurrent, rowsAll, rangeToCurrent } - -Expr rowNumber({ - List> partitionBy = const [], - List orderBy = const [], -}) => Expr._( - _WindowNode( - _Function('ROW_NUMBER', const []), - List.unmodifiable(partitionBy), - List.unmodifiable(orderBy), - null, - ), - Codecs.integer, -); -Expr rank({ - List> partitionBy = const [], - required List orderBy, -}) => Expr._( - _WindowNode( - _Function('RANK', const []), - List.unmodifiable(partitionBy), - List.unmodifiable(orderBy), - null, - ), - Codecs.integer, -); - -final class _WindowNode( - final _Node function, - final List> partition, - final List order, - final WindowFrame? frame, -) extends _Node { - @override - String writeSql(_Writer w) { - if (function case final _DecimalAverage average) { - return average.writeAverage(w, window: this); - } - final clauses = []; - if (partition.isNotEmpty) { - clauses.add( - 'PARTITION BY ${partition.map((e) => e._node.write(w)).join(', ')}', - ); - } - if (order.isNotEmpty) { - clauses.add('ORDER BY ${order.map((o) => o._write(w)).join(', ')}'); - } - if (frame != null) { - clauses.add(switch (frame!) { - WindowFrame.rowsToCurrent => - 'ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW', - WindowFrame.rowsAll => - 'ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING', - WindowFrame.rangeToCurrent => - 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW', - }); - } - return '${function.writeSql(w)} OVER (${clauses.join(' ')})'; - } -} - -bool _aggregate(_Node node) => switch (node) { - _WindowNode() || _Subquery() || _RelationSubquery() => false, - _DecimalAverage() => true, - _Function(:final name) - when {'COUNT', 'SUM', 'MIN', 'MAX', 'AVG'}.contains(name) => - true, - _ => _children(node).any(_aggregate), -}; -bool _window(_Node node) => node is _WindowNode || _children(node).any(_window); -List<_Node> _children(_Node node) => switch (node) { - _TemporalNode(:final child) || _TemporalCast(:final child) => [child], - _DecimalAverage(:final child) || _AverageInput(:final child) => [child], - _DecimalRatio(:final numerator, :final divisor) => [numerator, ?divisor], - _DecimalNode(:final child) => [child], - _DecimalCast(:final child) => [child], - _DecimalArithmetic(:final left, :final right) => [left, right], - _Binary(:final left, :final right) => [left, right], - _Unary(:final child) => [child], - _Function(:final arguments) => arguments, - _In(:final expression, :final values) => [expression, ...values], - _Raw(:final values) => values, - _WindowNode(:final function, :final partition, :final order) => [ - function, - ...partition.map((e) => e._node), - ...order.map((e) => e.expression._node), - ], - _RelationSubquery(:final relation) => [ - for (final p in relation._parent) p._node, - ], - _ => [], -}; - -void _validateGrouping(_QueryState state, _SelectionPlan plan) { - final optional = { - for (final join in state.joins) - if (join.left) join.alias.fields.table, - }; - bool unguarded(MapEntry, List>> entry) { - final absent = optional.difference(entry.key); - return entry.value.any((e) => _outerNullable(e._node, absent)); - } - - if (plan.required.any((e) => _outerNullable(e._node, optional)) || - plan.guarded.entries.any(unguarded)) { - throw const OrmException( - 'QUERY.NULLABILITY', - 'Use alias.optional(selection) or alias.nullable(expression) for outer-joined fields.', - ); - } - final predicates = [ - if (state.predicate case final p?) p._node, - for (final j in state.joins) j.on._node, - ]; - if (predicates.any((p) => _aggregate(p) || _window(p))) { - throw const OrmException( - 'QUERY.AGGREGATE', - 'WHERE and JOIN predicates cannot contain aggregate or window functions.', - ); - } - if (state.group.any((e) => _aggregate(e._node) || _window(e._node)) || - (state.having != null && _window(state.having!._node))) { - throw const OrmException( - 'QUERY.AGGREGATE', - 'Invalid aggregate/window placement.', - ); - } - final checked = [ - for (final e in plan.columns) e._node, - for (final o in state.order) o.expression._node, - if (state.having case final h?) h._node, - ]; - if (state.group.isEmpty && !checked.any(_aggregate)) { - if (state.having != null) { - throw const OrmException( - 'QUERY.AGGREGATE', - 'HAVING requires grouping or aggregation.', - ); - } - return; - } - bool grouped(_Node node) { - if (state.group.any((g) => _sameSqlNode(g._node, node))) return true; - if (node is _DecimalAverage) return true; - if (node is _Function && - {'COUNT', 'SUM', 'MIN', 'MAX', 'AVG'}.contains(node.name)) { - return true; - } - if (node is _ColumnNode || node is _Presence) return false; - if (node is _WindowNode) { - return [ - ..._children(node.function), - ...node.partition.map((e) => e._node), - ...node.order.map((e) => e.expression._node), - ].every(grouped); - } - return _children(node).every(grouped); - } - - if (!checked.every(grouped)) { - throw const OrmException( - 'QUERY.GROUPING', - 'Select grouped expressions or aggregates; ungrouped columns are ambiguous.', - ); - } -} - -bool _outerNullable(_Node node, Set optional) => switch (node) { - _ColumnNode(:final table) => optional.contains(table), - _WindowNode() || _Raw() => false, - _Function(name: 'COUNT') => false, - _Unary(op: 'IS NULL' || 'IS NOT NULL') => false, - _ => _children(node).any((child) => _outerNullable(child, optional)), -}; - -bool _sameSqlNode(_Node a, _Node b) { - if (identical(a, b)) return true; - if (a is _WindowNode && b is _WindowNode) { - return a.frame == b.frame && - _sameSqlNode(a.function, b.function) && - a.partition.length == b.partition.length && - a.partition.indexed.every( - (e) => _sameSqlNode(e.$2._node, b.partition[e.$1]._node), - ) && - a.order.length == b.order.length && - a.order.indexed.every((e) { - final other = b.order[e.$1]; - return e.$2.descending == other.descending && - e.$2.nulls == other.nulls && - _sameSqlNode(e.$2.expression._node, other.expression._node); - }); - } - return switch ((a, b)) { - ( - _TemporalNode(child: final ac, kind: final ak), - _TemporalNode(child: final bc, kind: final bk), - ) => - ak == bk && _sameSqlNode(ac, bc), - ( - _DecimalAverage(child: final ac, scale: final ax, rounding: final ar), - _DecimalAverage(child: final bc, scale: final bx, rounding: final br), - ) => - ax == bx && ar == br && _sameSqlNode(ac, bc), - ( - _DecimalRatio( - numerator: final an, - divisor: final ad, - scale: final ax, - rounding: final ar, - ), - _DecimalRatio( - numerator: final bn, - divisor: final bd, - scale: final bx, - rounding: final br, - ), - ) => - ax == bx && - ar == br && - _sameSqlNode(an, bn) && - (ad == null ? bd == null : bd != null && _sameSqlNode(ad, bd)), - ( - _DecimalCast(child: final ac, precision: final ap, scale: final asc), - _DecimalCast(child: final bc, precision: final bp, scale: final bs), - ) => - ap == bp && asc == bs && _sameSqlNode(ac, bc), - ( - _TemporalCast(child: final ac, kind: final ak, digits: final ap), - _TemporalCast(child: final bc, kind: final bk, digits: final bp), - ) => - ak == bk && ap == bp && _sameSqlNode(ac, bc), - (_DecimalNode(child: final ac), _DecimalNode(child: final bc)) => - _sameSqlNode(ac, bc), - ( - _DecimalArithmetic(left: final al, op: final ao, right: final ar), - _DecimalArithmetic(left: final bl, op: final bo, right: final br), - ) => - ao == bo && _sameSqlNode(al, bl) && _sameSqlNode(ar, br), - ( - _ColumnNode(table: final at, name: final an), - _ColumnNode(table: final bt, name: final bn), - ) => - at == bt && an == bn, - ( - _Parameter(value: final av, sqlType: final at), - _Parameter(value: final bv, sqlType: final bt), - ) => - av == bv && at == bt, - ( - _Function( - name: final an, - arguments: final aa, - distinct: final ad, - decimal: final ax, - ), - _Function( - name: final bn, - arguments: final ba, - distinct: final bd, - decimal: final bx, - ), - ) => - an == bn && - ax == bx && - ad == bd && - aa.length == ba.length && - aa.indexed.every((e) => _sameSqlNode(e.$2, ba[e.$1])), - ( - _Binary(left: final al, op: final ao, right: final ar), - _Binary(left: final bl, op: final bo, right: final br), - ) => - ao == bo && _sameSqlNode(al, bl) && _sameSqlNode(ar, br), - _ => false, - }; -} diff --git a/lib/src/cli/arguments.dart b/lib/src/cli/arguments.dart new file mode 100644 index 00000000..76960672 --- /dev/null +++ b/lib/src/cli/arguments.dart @@ -0,0 +1,25 @@ +(List, Map) parseOptions( + List args, + Set allowed, +) { + final positionals = [], options = {}; + for (var i = 0; i < args.length; i++) { + final argument = args[i]; + if (!argument.startsWith('--')) { + positionals.add(argument); + continue; + } + final key = argument.substring(2); + if (!allowed.contains(key) || options.containsKey(key)) { + throw FormatException('Unknown or repeated option --$key.'); + } + if (++i == args.length || args[i].startsWith('--')) { + throw FormatException('Missing value for --$key.'); + } + options[key] = args[i]; + } + return (positionals, options); +} + +String requiredOption(Map options, String key) => + options[key] ?? (throw FormatException('Missing --$key.')); diff --git a/lib/src/cli/commands.dart b/lib/src/cli/commands.dart index 18a71f1a..954961b5 100644 --- a/lib/src/cli/commands.dart +++ b/lib/src/cli/commands.dart @@ -1,10 +1,21 @@ -part of '../../cli.dart'; +import 'dart:convert'; +import 'dart:io'; -Future _runExplicitCli( - List arguments, { - bool json = false, -}) async { - void report(Map value) => _CliOutput(json).report(value); +import 'package:path/path.dart' as p; + +import '../../drivers/mysql.dart'; +import '../../drivers/mariadb.dart'; +import '../../drivers/postgres.dart'; +import '../../drivers/sqlite.dart'; +import '../../generate.dart'; +import '../../migrate.dart'; +import '../../runtime.dart'; +import '../sqlite/assets_io.dart'; +import 'arguments.dart'; +import 'output.dart'; + +Future runExplicitCli(List arguments, {bool json = false}) async { + void report(Map value) => CliOutput(json).report(value); try { if (arguments.first == 'web-assets') { if (arguments.length > 2 || @@ -17,7 +28,7 @@ Future _runExplicitCli( arguments.length == 2 ? arguments[1] : 'web/orm', ); await copySqliteWebAssets(directory); - _CliOutput(json).report({'copied': directory.path}); + CliOutput(json).report({'copied': directory.path}); return; } if (arguments.first == 'generate') { @@ -32,7 +43,7 @@ Future _runExplicitCli( arguments[1], output: arguments.length == 3 ? arguments[2] : null, ); - _CliOutput(json).report({ + CliOutput(json).report({ 'generated': arguments.length == 3 ? arguments[2] : p.setExtension(arguments[1], '.orm.dart'), @@ -79,7 +90,7 @@ Future _runExplicitCli( arguments[2], output: arguments.length == 4 ? arguments[3] : null, ); - _CliOutput(json).report({ + CliOutput(json).report({ 'generated': arguments.length == 4 ? arguments[3] : p.setExtension(arguments[2], '.queries.dart'), @@ -100,19 +111,24 @@ Future _runExplicitCli( 'queries check' => {...common, 'source', 'output'}, _ => throw FormatException('Unknown command: $command'), }; - final (positionals, options) = _parse(arguments.skip(2).toList(), flags); + final (positionals, options) = parseOptions( + arguments.skip(2).toList(), + flags, + ); if (positionals.isNotEmpty) { throw const FormatException('Unexpected positional arguments.'); } final queries = command == 'queries check' ? await checkGeneratedQueries( - _required(options, 'source'), + requiredOption(options, 'source'), output: options['output'], ) : null; - final table = command == 'db inspect' ? _required(options, 'table') : null; + final table = command == 'db inspect' + ? requiredOption(options, 'table') + : null; final importOutput = command == 'db import' - ? _required(options, 'output') + ? requiredOption(options, 'output') : ''; final importTables = options['table'] == null ? null : [options['table']!]; if (command == 'db import') { @@ -220,40 +236,15 @@ Future _runExplicitCli( await db.close(); } } on FormatException catch (error) { - _CliOutput(json).error(error.message, 64); + CliOutput(json).error(error.message, 64); } on ArgumentError catch (_) { - _CliOutput(json) + CliOutput(json) .error('Invalid command option or configuration. Use --help.', 64); } catch (error) { - _CliOutput(json).error(error.toString(), 1); - } -} - -(List, Map) _parse( - List args, - Set allowed, -) { - final positionals = [], options = {}; - for (var i = 0; i < args.length; i++) { - final argument = args[i]; - if (!argument.startsWith('--')) { - positionals.add(argument); - continue; - } - final key = argument.substring(2); - if (!allowed.contains(key) || options.containsKey(key)) { - throw FormatException('Unknown or repeated option --$key.'); - } - if (++i == args.length || args[i].startsWith('--')) { - throw FormatException('Missing value for --$key.'); - } - options[key] = args[i]; + CliOutput(json).error(error.toString(), 1); } - return (positionals, options); } -String _required(Map options, String key) => - options[key] ?? (throw FormatException('Missing --$key.')); SqlDialect _databaseDialect(Map options) { final selected = [ 'sqlite', @@ -295,14 +286,14 @@ Future> _open( }) async { final dialect = _databaseDialect(options); if (dialect == SqlDialect.sqlite) { - final file = _required(options, 'sqlite'); + final file = requiredOption(options, 'sqlite'); return SqlDatabase( await SqliteDriver.open( readOnly ? SqliteOptions.readOnly(file) : SqliteOptions.file(file), ), ); } - final name = _required(options, '${dialect.name}-env'); + final name = requiredOption(options, '${dialect.name}-env'); final value = Platform.environment[name]; if (value == null || value.isEmpty) { throw FormatException('Environment variable $name is empty or missing.'); diff --git a/lib/src/cli/config.dart b/lib/src/cli/config.dart new file mode 100644 index 00000000..ec9c5321 --- /dev/null +++ b/lib/src/cli/config.dart @@ -0,0 +1,48 @@ +import '../../migrate.dart'; +import '../../migrate_cli.dart'; + +/// Project paths, frozen migration history and database connection ownership. +/// +/// Pass this configuration to the project CLI. The history fixes one database +/// engine; changing a connection URL does not translate migrations. +final class OrmConfig { + /// Source declaration read by generation and migration creation. + final String schema; + + /// Generated client path, or the schema basename with `.orm.dart` when null. + final String? output; + + /// Directory containing reviewed migration files and their static registry. + final String migrations; + + /// Statically imported migrations and their reviewed fingerprints. + final MigrationHistory history; + + /// Desired physical schema used by inspection and migration commands. + /// + /// Migration creation regenerates the declaration before computing its diff. + final SchemaSnapshot snapshot; + + /// Connection factory for commands that inspect or change a database. + /// + /// Offline generation, history validation and recording never call it. + final MigrationConnection? connect; + + /// Explicit table and column renames applied when creating a migration. + final SchemaRenames renames; + + /// Table and column conversion expressions for reviewed type changes. + final Map> using; + + /// Configures a project without opening a database or executing a migration. + const OrmConfig({ + this.schema = 'lib/schema.dart', + this.output, + this.migrations = 'migrations', + required this.history, + required this.snapshot, + this.connect, + this.renames = const SchemaRenames(), + this.using = const {}, + }); +} diff --git a/lib/src/cli/init.dart b/lib/src/cli/init.dart index f5d32c69..a8b09b9e 100644 --- a/lib/src/cli/init.dart +++ b/lib/src/cli/init.dart @@ -1,11 +1,18 @@ -part of '../../cli.dart'; +import 'dart:io'; -Future _initializeProject(List args, _CliOutput output) async { - final (positionals, options) = _parse(args, {'database'}); +import 'package:dart_style/dart_style.dart'; + +import '../../generate.dart'; +import '../../migrate.dart'; +import 'arguments.dart'; +import 'output.dart'; + +Future initializeProject(List args, CliOutput output) async { + final (positionals, options) = parseOptions(args, {'database'}); if (positionals.isNotEmpty) { throw const FormatException('init accepts no positional arguments.'); } - final engine = _required(options, 'database'); + final engine = requiredOption(options, 'database'); if (!{'sqlite', 'postgres', 'mysql', 'mariadb'}.contains(engine)) { throw const FormatException( 'Choose --database sqlite, postgres, mysql or mariadb.', diff --git a/lib/src/cli/migration.dart b/lib/src/cli/migration.dart new file mode 100644 index 00000000..c9f61a9f --- /dev/null +++ b/lib/src/cli/migration.dart @@ -0,0 +1,300 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../../generate.dart'; +import '../../migrate.dart'; +import '../../runtime.dart'; + +/// Opens a runtime owned and closed by one migration CLI invocation. +/// +/// The factory must honor [readOnly] when requested and connect to the engine +/// declared by the migration history. Do not return a shared application runtime. +typedef MigrationConnection = FutureOr> Function({ + required bool readOnly, +}); + +/// Runs commands against a project's statically imported migration [history]. +/// +/// Offline commands never call [connect]. Database commands close the returned +/// runtime when finished. [json] selects machine-readable reports; saved schema +/// and migration files remain Dart source. Errors are written to stderr and set +/// the process exit code: 64 for usage errors, 2 for drift, or 1 for failure. +Future runMigrationCli( + List arguments, { + required MigrationHistory history, + required String directory, + SchemaSnapshot? schema, + MigrationConnection? connect, + SchemaRenames renames = const SchemaRenames(), + Map> using = const {}, + bool json = true, +}) async { + void report(Map value) { + if (json) { + stdout.writeln(const JsonEncoder.withIndent(' ').convert(value)); + } else { + for (final entry in value.entries) { + stdout.writeln( + '${entry.key}: ${entry.value is String ? entry.value : jsonEncode(entry.value)}', + ); + } + } + } + + void fail(String message, int code) { + stderr.writeln( + json ? jsonEncode({'error': message, 'exitCode': code}) : message, + ); + exitCode = code; + } + + try { + if (arguments.isEmpty || arguments.singleOrNull == '--help') { + stdout.writeln('''Migration commands: + create [--allow-destructive] + record Record a reviewed, unpublished edit + check + plan + apply [--max-backfill-batches ] + status + verify + baseline + inspect
+Connection, target schema, renames and conversions belong in the Dart entrypoint. +Rebuild static imports with: dart run orm migration registry '''); + return; + } + final command = arguments.first; + final rest = arguments.skip(1).toList(); + final expected = + command == 'create' || command == 'record' || command == 'inspect' + ? 1 + : 0; + var destructive = false; + int? batches; + if (command == 'create' && rest.lastOrNull == '--allow-destructive') { + destructive = true; + rest.removeLast(); + } + if (command == 'apply' && + rest.length == 2 && + rest.first == '--max-backfill-batches') { + batches = int.tryParse(rest.last); + if (batches == null || batches < 1) { + throw const FormatException('Use a positive batch count.'); + } + rest.clear(); + } + if (!{ + 'create', + 'record', + 'check', + 'plan', + 'apply', + 'status', + 'verify', + 'baseline', + 'inspect', + }.contains(command) || + rest.length != expected || + rest.any((value) => value.startsWith('--'))) { + throw const FormatException('Invalid migration command. Use --help.'); + } + if (command == 'record') { + final checksum = await recordMigration( + rest.single, + directory: directory, + history: history, + ); + report({'recorded': rest.single, 'checksum': checksum}); + return; + } + final migrations = history.checked; + if (command == 'check') { + report({ + 'valid': true, + 'dialect': history.dialect.name, + 'migrations': migrations.map((m) => m.id).toList(), + }); + return; + } + if (command == 'create') { + if (schema == null) { + throw const FormatException( + 'Provide the target schema in the Dart entrypoint.', + ); + } + final previous = migrations.lastOrNull; + if (previous != null && previous.snapshot == null) { + throw const FormatException( + 'The previous migration needs a historical schema.', + ); + } + final change = Migration.diff( + rest.single, + dialect: history.dialect, + from: previous?.snapshot ?? SchemaSnapshot([]), + to: schema, + previous: previous?.checksum, + renames: renames, + using: using, + allowDestructive: destructive, + ); + if (change.steps.isEmpty) { + report({'created': null, 'reason': 'Schema is unchanged.'}); + } else { + final path = await writeMigration( + change, + directory: directory, + history: history, + ); + report({'created': path, 'checksum': change.checksum}); + } + return; + } + if (command == 'verify' && schema == null) { + throw const FormatException( + 'Provide the expected schema in the Dart entrypoint.', + ); + } + if (command == 'baseline' && + (migrations.isEmpty || migrations.last.snapshot == null)) { + throw const FormatException( + 'Baseline requires a final historical schema.', + ); + } + if (connect == null) { + throw const FormatException('Provide a database connection factory.'); + } + final db = await connect( + readOnly: !{'apply', 'baseline'}.contains(command), + ); + try { + if (db.dialect != history.dialect) { + throw OrmException( + 'MIGRATION.TARGET', + 'History targets ${history.dialect.name}, connection is ${db.dialect.name}.', + ); + } + final migrator = Migrator(db); + switch (command) { + case 'plan': + final pending = await migrator.plan(migrations); + report({ + 'atomic': !pending.any( + (m) => m.steps.any( + (s) => s is CheckedSql || s is CheckedTableSql || s is Backfill, + ), + ), + 'pending': [ + for (final m in pending) + { + 'id': m.id, + 'checksum': m.checksum, + 'steps': m.steps.map((s) => s.toJson()).toList(), + }, + ], + 'progress': (await migrator.progress()) + .map((p) => p.toJson()) + .toList(), + }); + case 'apply': + final applied = await migrator.apply( + migrations, + maxBackfillBatches: batches, + ); + report({ + 'applied': applied, + if (batches != null) + 'complete': (await migrator.plan(migrations)).isEmpty, + }); + case 'status': + report({ + 'applied': [ + for (final m in await migrator.history()) + {'id': m.id, 'checksum': m.checksum}, + ], + 'progress': (await migrator.progress()) + .map((p) => p.toJson()) + .toList(), + }); + case 'baseline' || 'verify': + final result = command == 'baseline' + ? await migrator.baseline( + migrations, + expected: migrations.last.snapshot!, + ) + : await verifySchema(db, schema!); + report({ + 'matches': result.matches, + 'differences': result.differences, + 'unmanaged': [ + for (final o in result.unmanaged) + {'kind': o.kind, 'name': o.name, 'definition': o.definition}, + ], + }); + if (!result.matches) exitCode = 2; + case 'inspect': + final table = await inspectTable(db, rest.single); + report({ + 'table': table.name, + 'columns': [ + for (final c in table.columns) + { + 'name': c.name, + 'storageType': c.storageType, + 'nullable': c.nullable, + 'default': c.defaultSql, + 'generated': c.generated, + if (c.computed case final computed?) + 'computed': { + 'expression': computed.expression(db.dialect), + 'storage': computed.storage.name, + }, + if (c.integerBits != null) 'integerBits': c.integerBits, + if (c.decimalPrecision != null) + 'decimalPrecision': c.decimalPrecision, + if (c.decimalPrecision != null) + 'decimalScale': c.decimalScale ?? 0, + if (c.temporalPrecision != null) + 'temporalPrecision': c.temporalPrecision, + if (c.collation != null) 'collation': c.collation, + }, + ], + 'primaryKey': table.primaryKey, + 'uniqueKeys': table.uniqueKeys, + 'foreignKeys': [ + for (final k in table.foreignKeys) + { + 'columns': k.columns, + 'target': k.target, + 'targetColumns': k.targetColumns, + 'onDelete': k.onDelete, + }, + ], + 'indexes': [ + for (final i in table.indexes) + {'name': i.name, 'columns': i.columns, 'unique': i.unique}, + ], + 'checks': [ + for (final check in table.checks) + {'name': check.name, 'expression': check.expression}, + ], + 'unmanaged': [ + for (final o in table.unmanaged) + {'kind': o.kind, 'name': o.name, 'definition': o.definition}, + ], + }); + } + } finally { + await db.close(); + } + } on FormatException catch (error) { + fail(error.message, 64); + } on ArgumentError catch (_) { + fail('Invalid migration argument. Use --help.', 64); + } catch (error) { + fail(error.toString(), 1); + } +} diff --git a/lib/src/cli/output.dart b/lib/src/cli/output.dart new file mode 100644 index 00000000..d723a866 --- /dev/null +++ b/lib/src/cli/output.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; +import 'dart:io'; + +final class CliOutput(final bool json) { + void report(Map value) { + if (json) { + stdout.writeln(const JsonEncoder.withIndent(' ').convert(value)); + } else { + for (final entry in value.entries) { + stdout.writeln( + '${entry.key}: ${entry.value is String ? entry.value : jsonEncode(entry.value)}', + ); + } + } + } + + void error(String message, int code) { + stderr.writeln( + json ? jsonEncode({'error': message, 'exitCode': code}) : message, + ); + exitCode = code; + } +} diff --git a/lib/src/cli/runner.dart b/lib/src/cli/runner.dart new file mode 100644 index 00000000..3e492411 --- /dev/null +++ b/lib/src/cli/runner.dart @@ -0,0 +1,199 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../../generate.dart'; +import '../../migrate_cli.dart'; +import 'commands.dart'; +import 'config.dart'; +import 'init.dart'; +import 'output.dart'; + +const _help = { + '': '''Usage: dart run orm [--json] [--config orm.config.dart] + init --database sqlite|postgres|mysql|mariadb + generate [schema.dart] [output.orm.dart] + migrate create|check|plan|apply|status|verify|baseline|record|inspect + migration registry [--dialect engine] + db inspect|import + queries generate|check + web-assets [directory] + +Run help for details. Project commands use orm.config.dart. +Exit codes: 0 success, 1 execution failure, 2 drift/import issues, 64 usage error. +--json emits machine reports; schema and migration files always remain Dart.''', + 'init': '''Usage: dart run orm init --database sqlite|postgres|mysql|mariadb [--json] +Creates lib/schema.dart, its generated client/snapshot, a static migration +registry and orm.config.dart in an existing Dart project. Never replaces files, +connects to a database or applies DDL. Server URLs are read from DATABASE_URL +only when a connection command runs.''', + 'generate': + '''Usage: dart run orm generate [schema.dart] [output.orm.dart] [--json] +Without a path, uses OrmConfig.schema or lib/schema.dart. Generates the client +and standalone physical snapshot without connecting to a database. +Use an explicit schema path to recreate a deleted snapshot before loading config.''', + 'migrate': '''Usage: dart run orm migrate [--json] + create [--allow-destructive] Generate schema, then save a reviewed diff + check Validate fixed Dart migration history + plan Inspect pending steps without applying + apply [--max-backfill-batches n] Apply pending migrations + status Inspect applied history and checkpoints + verify Compare the catalog with the bundled snapshot + baseline Verify and register an existing schema + record Record an unpublished migration edit + inspect
Inspect physical table metadata +Configuration, fixed engine, connections, renames and conversions belong in +orm.config.dart. create/check/record do not connect. No command except explicit +apply/baseline changes database state. Review generated migrations before apply.''', + 'migration': '''Usage: dart run orm migration registry [--dialect sqlite|postgres|mysql|mariadb] +Rebuild static imports without updating reviewed migration fingerprints.''', + 'db': '''Usage: dart run orm db inspect --table name + dart run orm db import --output schema.dart [--table name] +Database: --sqlite file | --postgres-env NAME | --mysql-env NAME | --mariadb-env NAME +Server options: --tls verifyFull|require|disable; PostgreSQL: --database-schema name +Inspection and catalog import do not apply DDL or replace output files.''', + 'queries': '''Usage: dart run orm queries generate [output.queries.dart] + dart run orm queries check --source queries.dart [--output file] +Database options match help db. Generation is offline; check validates queries +against the selected database.''', + 'web-assets': '''Usage: dart run orm web-assets [directory] [--json] +Copies verified SQLite worker/WASM assets to web/orm by default. +Flutter Web bundles these resources automatically.''', +}; + +/// Runs package commands or a project entrypoint configured with [config]. +/// +/// When no configuration is passed, project commands load `orm.config.dart` in +/// a child Dart process. Help, initialization and explicit source generation do +/// not require a database. Reports go to stdout; failures go to stderr and set +/// the process exit code. Pass `--json` in [arguments] for machine-readable output. +Future runOrmCli(List arguments, {OrmConfig? config}) async { + final json = arguments.contains('--json'); + final output = CliOutput(json); + try { + final args = List.of(arguments); + if (args.where((a) => a == '--json').length > 1) { + throw const FormatException('Repeated option --json.'); + } + args.remove('--json'); + String? configPath; + final configIndex = args.indexOf('--config'); + if (configIndex >= 0) { + if (config != null || + args.where((a) => a == '--config').length > 1 || + configIndex + 1 >= args.length || + args[configIndex + 1].startsWith('--')) { + throw const FormatException( + 'Use --config once.', + ); + } + configPath = args[configIndex + 1]; + args.removeRange(configIndex, configIndex + 2); + } + if (args.isEmpty || + args.first == 'help' || + args.contains('--help') || + args.length == 1 && args.first == 'migrate') { + final topic = args.isEmpty || args.first == '--help' + ? '' + : args.first == 'help' + ? (args.length > 1 ? args[1] : '') + : args.first; + final help = _help[topic]; + if (help == null) throw FormatException('Unknown command: $topic.'); + if (json) { + output.report({'help': help}); + } else { + stdout.writeln(help); + } + return; + } + if (args.first == 'init') { + if (config != null || configPath != null) { + throw const FormatException( + 'Run init from the package CLI without --config.', + ); + } + await initializeProject(args.skip(1).toList(), output); + return; + } + final projectCommand = + args.first == 'migrate' || args.first == 'generate' && args.length == 1; + if (config == null && projectCommand) { + final path = configPath ?? 'orm.config.dart'; + if (await File(path).exists()) { + final child = await Process.start(Platform.resolvedExecutable, [ + 'run', + path, + ...args, + if (json) '--json', + ], mode: ProcessStartMode.inheritStdio); + exitCode = await child.exitCode; + return; + } + if (configPath != null || args.first == 'migrate') { + throw FormatException( + 'Missing $path. Run dart run orm init --database .', + ); + } + args.add('lib/schema.dart'); + } else if (configPath != null) { + throw const FormatException( + '--config applies to generate without a path and migrate commands.', + ); + } + if (config != null && args.first == 'generate' && args.length == 1) { + await writeGeneratedSchema(config.schema, output: config.output); + output.report({ + 'generated': + config.output ?? p.setExtension(config.schema, '.orm.dart'), + }); + return; + } + if (config != null && args.first == 'migrate') { + var snapshot = config.snapshot; + if (args.length > 1 && args[1] == 'create') { + if (!(args.length == 3 || + args.length == 4 && args.last == '--allow-destructive') || + args[2].startsWith('--')) { + throw const FormatException( + 'Use migrate create [--allow-destructive].', + ); + } + // Generate before diffing so an edited model cannot silently use the + // snapshot compiled into this invocation's configuration. + final generated = await generateSchema( + config.schema, + outputPath: config.output, + ); + final client = + config.output ?? p.setExtension(config.schema, '.orm.dart'); + await File(client).parent.create(recursive: true); + await File(client).writeAsString(generated.dart); + final snapshotPath = client.endsWith('.orm.dart') + ? '${client.substring(0, client.length - '.orm.dart'.length)}.snapshot.dart' + : p.setExtension(client, '.snapshot.dart'); + await File(snapshotPath).writeAsString(generated.snapshotDart); + snapshot = generated.snapshot; + } + await runMigrationCli( + args.skip(1).toList(), + history: config.history, + directory: config.migrations, + schema: snapshot, + connect: config.connect, + renames: config.renames, + using: config.using, + json: json, + ); + return; + } + await runExplicitCli(args, json: json); + } on FormatException catch (error) { + output.error(error.message, 64); + } on ArgumentError catch (_) { + output.error('Invalid command option or configuration. Use --help.', 64); + } catch (error) { + output.error(error.toString(), 1); + } +} diff --git a/lib/src/cte.dart b/lib/src/cte.dart deleted file mode 100644 index 9757c93f..00000000 --- a/lib/src/cte.dart +++ /dev/null @@ -1,124 +0,0 @@ -part of '../sql.dart'; - -abstract interface class _CteDefinition { - String get name; - String _writeDefinition(_Writer writer); -} - -/// A CTE exports SQL expression identities. Dart mapper properties are not -/// mistaken for SQL columns. ref() accepts only expressions the source selected. -final class Cte implements _CteDefinition { - final Query _source; - @override - final String name; - final _SelectionPlan _plan; - final _Decoder _decode; - late final Table> _table = _makeTable(); - - factory Cte._(Query source, String name) { - if (name.isEmpty) throw ArgumentError('CTE name cannot be empty.'); - final (plan, decode) = source._plan( - deduplicate: _sqlRowShape(source._selection) == null, - ); - if (plan.relations.isNotEmpty) { - throw const OrmException( - 'QUERY.CTE', - 'A CTE contains SQL columns, not batch-loaded relations.', - ); - } - return Cte._planned(source, name, plan, decode); - } - Cte._planned(this._source, this.name, this._plan, this._decode); - - Table> _makeTable() { - final optional = { - for (final join in [..._source._state.joins, ..._plan.joins]) - if (join.left) join.alias.fields.table, - }; - final nullable = [ - for (final column in _plan.columns) - column.codec.acceptsNull || _outerNullable(column._node, optional), - ]; - final schema = TableSchema( - name, - columns: [ - for (var i = 0; i < _plan.columns.length; i++) - Column( - 'c$i', - nullable[i] - ? _plan.columns[i].codec.nullable() - : _plan.columns[i].codec, - nullable: nullable[i], - ), - ], - ); - return Table( - schema, - (table) => CteFields._(table, _source._fields, _plan, nullable), - (fields) => _ReboundSelection(_decode, [ - for (var i = 0; i < _plan.columns.length; i++) - Expr._(_ColumnNode(fields.table, 'c$i'), schema.columns[i].codec), - ], source: _source._selection), - ); - } - - Query> get query { - final fields = _table.createFields(TableRef(_table.schema)); - return Query._( - _source.database, - fields, - _QueryState(fields.table, ctes: [this]), - _table.selectRow(fields), - ); - } - - TableAlias> alias() => TableAlias._(_table, this); - - @override - String _writeDefinition(_Writer writer) => - '${writer.quote(name)} ' - '(${[for (var i = 0; i < _plan.columns.length; i++) writer.quote('c$i')].join(', ')}) ' - 'AS (${_source._write(writer, _plan)})'; -} - -final class CteFields extends Fields { - final F _original; - final _SelectionPlan _plan; - final List _nullable; - CteFields._(super.table, this._original, this._plan, this._nullable); - - Expr ref(Expr Function(F) expression) { - final original = expression(_original); - final index = _plan.columns.indexWhere( - (e) => _sameSqlNode(e._node, original._node), - ); - if (index < 0) { - throw const OrmException( - 'QUERY.CTE_COLUMN', - 'The CTE did not export this SQL expression.', - ); - } - if (_nullable[index] && !original.codec.acceptsNull) { - throw const OrmException( - 'QUERY.NULLABILITY', - 'This CTE column is nullable; reference it with a nullable expression.', - ); - } - return Expr._(_ColumnNode(table, 'c$index'), original.codec); - } -} - -final class _ReboundSelection( - final _Decoder decode, - final List> columns, { - final Selection? source, -}) extends Selection { - @override - _Decoder _bind(_SelectionPlan plan) { - for (final column in columns) { - plan.require(column); - } - final indices = [for (final column in columns) plan.column(column)]; - return (row) => decode([for (final index in indices) row[index]]); - } -} diff --git a/lib/src/decimal_average.dart b/lib/src/decimal_average.dart deleted file mode 100644 index e0c4940e..00000000 --- a/lib/src/decimal_average.dart +++ /dev/null @@ -1,123 +0,0 @@ -part of '../sql.dart'; - -final class _DecimalAverage( - final _Node child, - final int scale, - final DecimalRounding rounding, -) extends _Node { - @override - String writeSql(_Writer w) => writeAverage(w); - - String writeAverage(_Writer w, {_WindowNode? window}) { - if (w.mysql) { - throw const OrmException( - 'CAPABILITY.DECIMAL_ROUNDING', - 'Exact decimal averages are not supported on MySQL/MariaDB.', - ); - } - if (w.averageInputs == null) { - throw const OrmException( - 'QUERY.AGGREGATE', - 'An average requires a SELECT query; use a scalar subquery in assignments.', - ); - } - if (w.dialect == SqlDialect.sqlite) { - final function = _Function('orm_decimal_avg_v1', [ - child, - _Parameter(scale), - _Parameter(rounding.index), - ]); - return (window == null - ? function - : _WindowNode( - function, - window.partition, - window.order, - window.frame, - )) - .writeSql(w); - } - final input = _AverageInput(child); - final partition = [ - for (final e in window?.partition ?? >[]) - Expr._(_AverageInput(e._node), e.codec), - ]; - final order = [ - for (final o in window?.order ?? []) - OrderTerm._( - Expr._(_AverageInput(o.expression._node), o.expression.codec), - o.descending, - o.nulls, - ), - ]; - String aggregate(String name, _Node argument) { - final function = _Function(name, [argument]); - return (window == null - ? function - : _WindowNode(function, partition, order, window.frame)) - .write(w); - } - - // B exceeds every possible COUNT (signed int64). Each SUM stays finite: - // high has 20 fewer integer digits; low is bounded by COUNT*B. Native SUM - // preserves its exact accumulator without holding an array of input rows. - const base = "NUMERIC '1e20'"; - final high = aggregate('SUM', _Raw(['div(', ', $base)'], [input])); - final low = aggregate('SUM', _Raw(['mod(', ', $base)'], [input])); - final count = aggregate('COUNT', input); - final prefix = - ''' -WITH orm_mean_args(h, l, n) AS MATERIALIZED ( - VALUES ($high, $low, CAST($count AS NUMERIC)) -), orm_mean_high AS MATERIALIZED ( - SELECT *, div(h, NULLIF(n, 0)) AS qh, mod(h, NULLIF(n, 0)) AS rh - FROM orm_mean_args -), orm_mean_tail AS MATERIALIZED ( - SELECT *, rh * $base + l AS t FROM orm_mean_high -), orm_mean_parts AS MATERIALIZED ( - SELECT n, qh * $base + div(t, NULLIF(n, 0)) AS q, - mod(t, NULLIF(n, 0)) AS r FROM orm_mean_tail -), orm_mean_normal AS MATERIALIZED ( - SELECT n, - CASE WHEN q > 0 AND r < 0 THEN q - 1 WHEN q < 0 AND r > 0 THEN q + 1 ELSE q END AS q, - CASE WHEN q > 0 AND r < 0 THEN r + n WHEN q < 0 AND r > 0 THEN r - n ELSE r END AS r - FROM orm_mean_parts -), orm_decimal_remainder AS MATERIALIZED ( - SELECT abs(q) AS q, abs(r) AS r, n AS b, n = 0 AS nil, - CASE WHEN q = 0 THEN sign(r) ELSE sign(q) END AS sgn - FROM orm_mean_normal -)'''; - return _DecimalRoundSql(scale, rounding).write(prefix); - } -} - -// Shared by the high/low/count aggregates (and by a window's frame expressions). -// Projection staging handles windows; ordinary aggregation uses a row lateral. -final class _AverageInput(final _Node child) extends _Node { - @override - String writeSql(_Writer w) => w.averageInputs!.capture(this); -} - -final class _AverageInputs(final _Writer writer, final String anchor) { - final List joins = []; - final Map<_AverageInput, String> _references = {}; - String capture(_AverageInput input) => _references.putIfAbsent(input, () { - final name = writer.quote( - '_orm_avg_${writer.aliases.length}_${joins.length}', - ); - final saved = writer.project; - writer.project = null; - late String value; - try { - value = input.child.write(writer); - } finally { - writer.project = saved; - } - // The zero term keeps even a volatile, otherwise uncorrelated input tied - // to each source row. OFFSET prevents flattening/repeated evaluation. - joins.add( - ' CROSS JOIN LATERAL (SELECT ($value) + CAST($anchor IS NULL AS INT) * NUMERIC \'0\' AS v OFFSET 0) AS $name', - ); - return '$name.v'; - }); -} diff --git a/lib/src/decimal_division.dart b/lib/src/decimal_division.dart deleted file mode 100644 index 316be215..00000000 --- a/lib/src/decimal_division.dart +++ /dev/null @@ -1,133 +0,0 @@ -part of '../sql.dart'; - -/// A null divisor denotes rounding one value. Operands are evaluated once. -final class _DecimalRatio( - final _Node numerator, - final _Node? divisor, - final int scale, - final DecimalRounding rounding, -) extends _Node { - @override - String writeSql(_Writer w) { - if (w.mysql) { - throw const OrmException( - 'CAPABILITY.DECIMAL_ROUNDING', - 'Exact SQL division and rounding are not implemented for MySQL/MariaDB; use an explicit DECIMAL expression.', - ); - } - final a = numerator.write(w), b = divisor?.write(w); - if (w.dialect == SqlDialect.sqlite) { - return b == null - ? 'orm_decimal_round_v1($a, $scale, ${rounding.index})' - : 'orm_decimal_div_v1($a, $b, $scale, ${rounding.index})'; - } - // MATERIALIZED keeps volatile expressions and correlated operands from - // being reevaluated by the quotient, remainder and rounding calculations. - final prefix = - ''' -WITH orm_decimal_args(a, b) AS MATERIALIZED ( - VALUES (CAST($a AS NUMERIC), CAST(${b ?? '1'} AS NUMERIC)) -), orm_decimal_parts AS MATERIALIZED ( - SELECT abs(a) AS a, abs(b) AS b, sign(a) * sign(b) AS sgn, - a IS NULL OR b IS NULL AS nil, - div(abs(a), NULLIF(abs(b), 0)) AS q - FROM orm_decimal_args -), orm_decimal_remainder AS MATERIALIZED ( - SELECT *, a - q * b AS r FROM orm_decimal_parts -)'''; - return _DecimalRoundSql(scale, rounding).write(prefix); - } -} - -// The input CTE exposes a nonnegative integer quotient q, remainder r < b, -// positive denominator b, result sign sgn and empty/null marker nil. -final class _DecimalRoundSql(final int scale, final DecimalRounding rounding) { - String write(String prefix) => - scale >= 0 ? _positive(prefix) : _negative(prefix); - - String _positive(String prefix) { - final factor = "NUMERIC '1e$scale'"; - final unit = "NUMERIC '1e-$scale'"; - final text = scale == 0 - ? 'q::text' - : "q::text || '.' || lpad(f::text, $scale, '0')"; - final increment = _increment( - nonzero: 'r <> 0', - above: 'r > b - r', - tie: 'r = b - r', - odd: 'mod(${scale == 0 ? 'q' : 'f'}, 2) <> 0', - ); - // Usually r*factor fits NUMERIC. For a huge remainder, split b=d*factor+e - // instead. Then c=div(r,d) overestimates div(r*factor,b) by at most one: - // the fallback implies d >= 10^(131072-2*scale) > factor^2. All products - // below stay within the original denominator's magnitude or factor^2. - return '''($prefix, -orm_decimal_scaled AS MATERIALIZED ( - SELECT *, length(trunc(r)::text) + $scale <= ${Decimal.maxIntegerDigits} AS simple - FROM orm_decimal_remainder -), orm_decimal_split AS MATERIALIZED ( - SELECT *, CASE WHEN simple THEN r * $factor ELSE r END AS n, - CASE WHEN simple THEN b ELSE div(b, $factor) END AS d, - CASE WHEN simple THEN 0 ELSE mod(b, $factor) END AS e - FROM orm_decimal_scaled -), orm_decimal_estimate AS MATERIALIZED ( - SELECT *, div(n, NULLIF(d, 0)) AS c FROM orm_decimal_split -), orm_decimal_correction AS MATERIALIZED ( - SELECT *, CASE WHEN simple THEN n - c * d - ELSE (n - c * d) * $factor - c * e END AS tail - FROM orm_decimal_estimate -), orm_decimal_final AS ( - SELECT q, b, sgn, nil, - CASE WHEN tail < 0 THEN c - 1 ELSE c END AS f, - CASE WHEN tail < 0 THEN tail + b ELSE tail END AS r - FROM orm_decimal_correction -) -SELECT CASE WHEN nil THEN NULL WHEN b = 0 THEN 1 / b - ${_inexact('r <> 0')} - ELSE sgn * (CAST($text AS NUMERIC) + CASE WHEN $increment THEN $unit ELSE 0 END) -END FROM orm_decimal_final)'''; - } - - String _negative(String prefix) { - final places = -scale, half = "NUMERIC '5e${-scale - 1}'"; - final increment = _increment( - nonzero: 'lost <> 0 OR r <> 0', - above: 'lost > $half OR (lost = $half AND r <> 0)', - tie: 'lost = $half AND r = 0', - odd: 'mod(kept, 2) <> 0', - ); - // Construct integer digits as text so even the outermost supported scale - // (-131072) can return zero without first constructing an overflowing unit. - return '''($prefix, -orm_decimal_integer AS MATERIALIZED ( - SELECT *, CAST(COALESCE(NULLIF(left(q::text, greatest(length(q::text) - $places, 0)), ''), '0') AS NUMERIC) AS kept - FROM orm_decimal_remainder -), orm_decimal_truncated AS MATERIALIZED ( - SELECT *, CAST(kept::text || repeat('0', $places) AS NUMERIC) AS truncated - FROM orm_decimal_integer -), orm_decimal_final AS ( - SELECT *, q - truncated AS lost FROM orm_decimal_truncated -) -SELECT CASE WHEN nil THEN NULL WHEN b = 0 THEN 1 / b - ${_inexact('lost <> 0 OR r <> 0')} - ELSE sgn * CAST((kept + CASE WHEN $increment THEN 1 ELSE 0 END)::text || repeat('0', $places) AS NUMERIC) -END FROM orm_decimal_final)'''; - } - - String _inexact(String nonzero) => rounding == DecimalRounding.exact - ? "WHEN $nonzero THEN CAST(q::text || '_decimal_rounding_required' AS NUMERIC)" - : ''; - - String _increment({ - required String nonzero, - required String above, - required String tie, - required String odd, - }) => switch (rounding) { - DecimalRounding.exact || DecimalRounding.towardZero => 'FALSE', - DecimalRounding.floor => 'sgn < 0 AND ($nonzero)', - DecimalRounding.ceiling => 'sgn > 0 AND ($nonzero)', - DecimalRounding.halfAwayFromZero => '($above) OR ($tie)', - DecimalRounding.halfEven => '($above) OR (($tie) AND ($odd))', - }; -} diff --git a/lib/src/decimal_sql.dart b/lib/src/decimal_sql.dart deleted file mode 100644 index aded9a5e..00000000 --- a/lib/src/decimal_sql.dart +++ /dev/null @@ -1,162 +0,0 @@ -part of '../sql.dart'; - -// Attach collation to every decimal expression, including computed projections, -// CTE references and UNION outputs. Native PostgreSQL NUMERIC needs no wrapper. -final class _DecimalNode(final _Node child) extends _Node { - @override - String writeSql(_Writer w) { - if (!w.exactDecimal) { - throw const OrmException( - 'CAPABILITY.DECIMAL', - 'This driver does not provide exact decimal SQL.', - ); - } - final sql = child.write(w); - return w.dialect == SqlDialect.sqlite - ? '($sql COLLATE "orm_decimal_v1")' - : sql; - } -} - -final class _DecimalCast( - final _Node child, - final int precision, - final int scale, { - final bool columnAssignment = false, -}) extends _Node { - @override - String writeSql(_Writer w) { - Decimal.validateDigits(precision, scale); - if (!w.exactDecimal) { - throw const OrmException( - 'CAPABILITY.DECIMAL', - 'This driver does not provide exact decimal SQL.', - ); - } - final expression = child.write(w); - if (w.mysql) { - if (precision > 65 || scale < 0 || scale > 30 || scale > precision) { - throw const OrmException( - 'CAPABILITY.DECIMAL', - 'MySQL/MariaDB decimals require precision 1–65 and scale 0–min(precision, 30).', - ); - } - if (columnAssignment) return expression; - throw const OrmException( - 'CAPABILITY.DECIMAL_PRECISION', - 'MySQL/MariaDB decimal casts may clamp overflow; use explicit native SQL when its precision is acceptable.', - ); - } - return w.dialect == SqlDialect.postgres - ? 'CAST($expression AS NUMERIC($precision,$scale))' - : 'orm_decimal_cast_v1($expression, $precision, $scale)'; - } -} - -final class _DecimalArithmetic( - final _Node left, - final String op, - final _Node right, -) extends _Node { - @override - String writeSql(_Writer w) { - if (w.mysql) { - throw const OrmException( - 'CAPABILITY.DECIMAL_PRECISION', - 'MySQL/MariaDB arithmetic may silently truncate decimal digits; use explicit native SQL when its precision is acceptable.', - ); - } - final a = left.write(w), b = right.write(w); - if (w.dialect != SqlDialect.sqlite) return '($a $op $b)'; - final name = switch (op) { - '+' => 'add', - '-' => 'sub', - '*' => 'mul', - _ => throw StateError('Invalid decimal operation'), - }; - return 'orm_decimal_${name}_v1($a, $b)'; - } -} - -extension DecimalExpression on Expr { - /// Averages non-null inputs, rounding once at the requested result scale. - /// Empty/all-null input returns null; the default rejects lost digits. - Expr average({ - required int scale, - DecimalRounding rounding = DecimalRounding.exact, - }) { - Decimal.validateScale(scale); - if (_window(_node)) { - throw const OrmException( - 'QUERY.WINDOW', - 'Project a window result through a CTE before averaging it.', - ); - } - return Expr._( - _DecimalAverage(_node, scale, rounding), - Codecs.decimal.nullable(), - ); - } - - /// Rounds in SQL; [DecimalRounding.exact] rejects any lost nonzero digits. - Expr rounded( - int scale, { - DecimalRounding rounding = DecimalRounding.exact, - }) { - Decimal.validateScale(scale); - return Expr._(_DecimalRatio(_node, null, scale, rounding), codec); - } - - /// Divides in SQL to an explicit scale without a floating-point intermediate. - Expr divide( - Decimal divisor, { - required int scale, - DecimalRounding rounding = DecimalRounding.exact, - }) { - Decimal.validateScale(scale); - return Expr._( - _DecimalRatio( - _node, - value(divisor, Codecs.decimal)._node, - scale, - rounding, - ), - codec, - ); - } - - /// Divides two SQL expressions. A null operand produces null. - Expr divideExpression( - Expr divisor, { - required int scale, - DecimalRounding rounding = DecimalRounding.exact, - }) { - Decimal.validateScale(scale); - return Expr._( - _DecimalRatio(_node, divisor._node, scale, rounding), - Codecs.decimal.nullable(), - ); - } - - Expr constrained(int precision, int scale) { - Decimal.validateDigits(precision, scale); - return Expr._(_DecimalCast(_node, precision, scale), codec); - } - - Expr plus(Decimal n) => _arithmetic('+', value(n, Codecs.decimal)); - Expr minus(Decimal n) => _arithmetic('-', value(n, Codecs.decimal)); - Expr times(Decimal n) => _arithmetic('*', value(n, Codecs.decimal)); - Expr plusExpression(Expr other) => _binary('+', other); - Expr minusExpression(Expr other) => _binary('-', other); - Expr timesExpression(Expr other) => _binary('*', other); - Expr _arithmetic(String op, Expr other) => - Expr._(_DecimalArithmetic(_node, op, other._node), codec); - Expr _binary(String op, Expr other) => Expr._( - _DecimalArithmetic(_node, op, other._node), - Codecs.decimal.nullable(), - ); - Expr sum({bool distinct = false}) => Expr._( - _Function('SUM', [_node], decimal: true, distinct: distinct), - Codecs.decimal.nullable(), - ); -} diff --git a/lib/src/driver/driver.dart b/lib/src/driver/driver.dart new file mode 100644 index 00000000..df85e6d0 --- /dev/null +++ b/lib/src/driver/driver.dart @@ -0,0 +1,328 @@ +import '../../values.dart'; + +/// SQL engine used for quoting, parameter syntax, and capability validation. +enum SqlDialect { + /// SQLite on native and browser platforms. + sqlite, + + /// PostgreSQL with its own SQL and type semantics. + postgres, + + /// MySQL, distinct from MariaDB for schema and capability checks. + mysql, + + /// MariaDB, distinct from MySQL for schema and capability checks. + mariadb, +} + +/// SQL text and separately bound values. Values are never interpolated into SQL. +final class SqlCommand { + /// SQL text using the target driver's parameter placeholder syntax. + final String sql; + + /// Bound values in placeholder order; the constructor copies this list. + final List parameters; + + /// Creates a command without parsing SQL or opening a connection. + SqlCommand(this.sql, [List parameters = const []]) + : parameters = List.unmodifiable(parameters); +} + +/// Static engine identity used to constrain driver and transaction APIs. +/// +/// Values do not hold connections or configuration; use the corresponding +/// driver options when opening a database. +sealed class Backend { + /// Creates an engine type marker. + const Backend(); +} + +/// Type marker for PostgreSQL drivers and sessions. +final class Postgres extends Backend { + /// Creates a PostgreSQL type marker without opening a connection. + const Postgres(); +} + +/// Type marker for native and browser SQLite drivers and sessions. +final class Sqlite extends Backend { + /// Creates a SQLite type marker without opening a database. + const Sqlite(); +} + +/// Type marker for MySQL drivers and sessions. +final class Mysql extends Backend { + /// Creates a MySQL type marker without opening a connection. + const Mysql(); +} + +/// Type marker for MariaDB drivers and sessions. +final class Mariadb extends Backend { + /// Creates a MariaDB type marker without opening a connection. + const Mariadb(); +} + +/// Features a concrete driver can execute for its actual engine and platform. +/// +/// Query and runtime layers check these limits before submitting unsupported +/// work. A database product may support more than its current adapter exposes. +/// +/// {@category Drivers} +final class Capabilities { + /// Engine syntax and semantics used by this connection. + final SqlDialect dialect; + + /// Maximum bound parameter count for a single statement. + final int maxParameters; + + /// Whether writes support returning selected rows directly. + final bool returning; + + /// Whether the adapter supports SQL window expressions. + final bool windowFunctions; + + /// Whether callers can fetch bounded batches through [SqlCursor]. + final bool streaming; + + /// Whether an active statement can receive a cancellation request. + final bool cancellation; + + /// Honors statement deadlines, potentially by discarding the connection. + /// A timeout does not prove that a submitted write was rolled back. + final bool statementTimeout; + + /// Whether the adapter supplies the ORM's exact decimal SQL behavior. + final bool exactDecimal; + + /// Whether exact calendar and UTC instant storage conversions are configured. + final bool temporal; + + /// Declares adapter capabilities; deadlines default to cancellation support. + const Capabilities({ + required this.dialect, + required this.maxParameters, + this.returning = true, + this.windowFunctions = true, + this.streaming = false, + this.cancellation = false, + bool? statementTimeout, + this.exactDecimal = false, + this.temporal = false, + }) : statementTimeout = statementTimeout ?? cancellation; +} + +/// Raw row values and write metadata returned by a completed driver operation. +final class SqlResult { + /// Rows in result order, with values in [columns] order when names are known. + final List> rows; + + /// Physical result column labels, or an empty list if unavailable. + final List columns; + + /// Number of affected rows reported by the engine; defaults to zero. + final int affectedRows; + + /// Generated identity returned directly by the server, when available. + final int? lastInsertId; + + /// Retains the supplied row lists without copying them. + const SqlResult( + this.rows, { + this.columns = const [], + this.affectedRows = 0, + this.lastInsertId, + }); +} + +/// Execution port for one physical connection during an active driver lease. +/// +/// Await each operation before issuing another one on the same connection. +/// Connection ownership remains with the [Driver]; application code uses [Driver.run] +/// through that driver rather than retaining the connection beyond its callback. +/// +/// {@category Drivers} +abstract interface class SqlConnection { + /// Actual adapter state after the last completed request; null if unavailable. + bool? get transactionActive; + + /// Executes SQL with separately bound parameters and per-statement controls. + Future execute( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }); + + /// Opens a bounded-fetch cursor when the driver supports streaming. + /// + /// Engine-specific transaction requirements still apply. Close the cursor + /// before returning its connection lease. + Future openCursor( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }); + + /// Permanently discard a connection whose protocol or transaction is uncertain. + Future invalidate(); +} + +/// Owns or borrows connection resources for a specific engine [B]. +/// +/// Implementations lease one connection for each [run] callback and return it +/// after the callback settles. The driver contract does not imply that a callback +/// is a transaction: use a SQL runtime or explicitly issue transaction commands. +/// +/// ```dart +/// Future countUsers(Driver driver) => driver.run((connection) async { +/// final result = await connection.execute(SqlCommand('SELECT COUNT(*) FROM users')); +/// return result.rows.single.single as int; +/// }); +/// ``` +/// +/// {@category Drivers} +abstract interface class Driver { + /// Features and limits exposed by this adapter and its actual backend. + Capabilities get capabilities; + + /// Leases one connection until [action] completes or throws. + /// + /// Do not retain the connection or start unawaited work beyond the callback. + Future run(Future Function(SqlConnection) action); + + /// Rejects new work and releases resources owned by this driver. + /// + /// Borrowed external pools remain owned by the caller. Concrete adapters + /// document how already accepted work is drained before completion. + Future close(); +} + +/// A cancellation request. Await the operation itself to observe its actual +/// database outcome; completion can win a race with cancellation. +final class CancellationToken { + bool _cancelled = false; + final Set _listeners = {}; + + /// Whether cancellation has been requested; it does not describe SQL outcome. + bool get isCancelled => _cancelled; + + /// Requests cancellation once and notifies all currently registered listeners. + void cancel() { + if (_cancelled) return; + _cancelled = true; + for (final listener in _listeners.toList()) { + _notify(listener); + } + _listeners.clear(); + } + + /// Adapter hook; returns a function that removes the listener. + /// Listener errors are isolated so every pending operation receives cancellation. + void Function() listen(void Function() listener) { + if (_cancelled) { + _notify(listener); + return () {}; + } + _listeners.add(listener); + return () => _listeners.remove(listener); + } + + void _notify(void Function() listener) { + try { + listener(); + } catch (_) {} + } +} + +/// Per-operation connection acquisition and statement execution controls. +/// +/// Acquisition and statement deadlines are independent. A deadline expiring after +/// submission does not establish whether a write committed or was rolled back. +final class ExecutionOptions { + /// Optional cancellation request shared by acquisition and execution. + final CancellationToken? cancellation; + + /// Time allowed to acquire a lease. Independent of statement [timeout]. + final Duration? acquireTimeout; + + /// Limit for each database statement/fetch, not time spent consuming rows. + final Duration? timeout; + + /// Creates optional controls; omitted deadlines use runtime or driver defaults. + const ExecutionOptions({ + this.cancellation, + this.timeout, + this.acquireTimeout, + }); + + /// Connection-wait controls derived without including the statement deadline. + AcquisitionOptions get acquisition => + acquireTimeout == null && cancellation == null + ? const AcquisitionOptions() + : AcquisitionOptions(timeout: acquireTimeout, cancellation: cancellation); + + /// Rejects non-positive deadlines and cancellation requested before execution. + void check() { + if (acquireTimeout != null && acquireTimeout! <= Duration.zero) { + throw ArgumentError.value(acquireTimeout, 'acquireTimeout'); + } + if (timeout != null && timeout! <= Duration.zero) { + throw ArgumentError.value(timeout, 'timeout'); + } + if (cancellation?.isCancelled ?? false) { + throw const OrmException( + 'OPERATION.CANCELLED', + 'Cancelled before starting a database operation.', + ); + } + } +} + +/// Limits waiting for a connection, including establishment and initialization. +/// Cancellation after acquisition does not cancel the session callback. +final class AcquisitionOptions { + /// Optional positive deadline for obtaining a usable connection lease. + final Duration? timeout; + + /// Cancellation request observed only while waiting for acquisition. + final CancellationToken? cancellation; + + /// Creates connection-wait controls independent of statement deadlines. + const AcquisitionOptions({this.timeout, this.cancellation}); + + /// Rejects an invalid deadline or cancellation already requested. + void check() { + if (timeout != null && timeout! <= Duration.zero) { + throw ArgumentError.value(timeout, 'acquireTimeout'); + } + if (cancellation?.isCancelled ?? false) { + throw const OrmException( + 'OPERATION.CANCELLED', + 'Connection acquisition cancelled.', + ); + } + } +} + +/// Bounded row retrieval tied to the connection lease that opened it. +abstract interface class SqlCursor { + /// Fetches at most [count] rows; an empty batch marks the end of the result. + /// + /// [count] must be positive. Await a fetch before requesting the next batch. + Future fetch( + int count, { + ExecutionOptions options = const ExecutionOptions(), + }); + + /// Releases the cursor; cleanup is safe to request more than once. + Future close(); +} + +/// Adapter classification. Retrying also requires a confirmed rollback and an +/// explicitly repeatable application callback; these flags alone are not enough. +abstract interface class SqlFailure implements Exception { + /// Whether this failure can be considered for a complete transaction retry. + bool get retryTransaction; + + /// Retry the same COMMIT only while the adapter confirms the transaction is active. + bool get retryCommit; + + /// Whether the error establishes that this COMMIT was rejected by the server. + bool get commitRejected; +} diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart index 0727f607..fc2466ed 100644 --- a/lib/src/generate/build.dart +++ b/lib/src/generate/build.dart @@ -1,4 +1,13 @@ -part of '../generate.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart' as builder; +import 'package:path/path.dart' as p; + +import 'exception.dart'; +import 'queries.dart'; +import 'schema.dart'; /// Factory used by build_runner's build.yaml registration. builder.Builder ormBuilder(builder.BuilderOptions options) { @@ -34,7 +43,7 @@ final class _OrmQueryBuilder implements builder.Builder { } final output = input.changeExtension('.queries.dart'); final (unit, library) = await const _OrmBuilder()._resolveSchema(step); - final result = await _generateQueries( + final result = await generateResolvedQueries( unit, library, p.url.relative(input.path, from: p.url.dirname(output.path)), @@ -71,7 +80,7 @@ final class _OrmBuilder implements builder.Builder { } final output = input.changeExtension('.orm.dart'); final (unit, library) = await _resolveSchema(step); - final result = _generate( + final result = generateResolvedSchema( unit, library, p.url.relative(input.path, from: p.url.dirname(output.path)), diff --git a/lib/src/generate/emitter.dart b/lib/src/generate/emitter.dart index bf6d4985..ef64ee82 100644 --- a/lib/src/generate/emitter.dart +++ b/lib/src/generate/emitter.dart @@ -1,41 +1,44 @@ -part of '../generate.dart'; +import '../../schema_model.dart'; +import 'model.dart'; +import 'source.dart'; +import 'types.dart'; -String _emit(List<_Entity> schema, String import, _DartNames names) { +String emitSchema(List schema, String import, DartNames names) { final b = StringBuffer('// GENERATED CODE - DO NOT MODIFY BY HAND.\n\n') ..writeln("import 'package:orm/sql.dart';") - ..writeln("import ${_literal(import)} as models;") + ..writeln("import ${dartLiteral(import)} as models;") ..writeln( - "export ${_literal(import)} show ${schema.map((e) => e.row).toSet().join(', ')};", + "export ${dartLiteral(import)} show ${schema.map((e) => e.row).toSet().join(', ')};", ); if (names.typedData) { b.writeln("import 'dart:typed_data';"); } for (final (uri, prefix) in names.imports) { - b.writeln('import ${_literal(uri)} as $prefix;'); + b.writeln('import ${dartLiteral(uri)} as $prefix;'); } b.writeln(''); for (final entity in schema) { for (final f in entity.fields) { b.writeln( - 'final ${_columnSymbol(entity, f)} = Column<${f.type}>(${_literal(f.column)}, ${f.codec}, ' - 'nullable: ${f.nullable}, generated: ${f.generated}${f.defaultSql == null ? '' : ', defaultSql: ${_literal(f.defaultSql!)}'}${f.clientDefault == null ? '' : ', clientDefault: ${f.clientDefault}'}${f.computed == null ? '' : ', computed: ${_computedLiteral(f.computed!)}'}${f.integerBits == null || f.integerBits == 64 ? '' : ', integerBits: ${f.integerBits}'}${f.temporalPrecision == null || f.temporalPrecision == 6 ? '' : ', temporalPrecision: ${f.temporalPrecision}'}${f.decimalPrecision == null ? '' : ', decimalPrecision: ${f.decimalPrecision}'}${f.decimalScale == null || f.decimalScale == 0 ? '' : ', decimalScale: ${f.decimalScale}'});', + 'final ${columnSymbol(entity, f)} = Column<${f.type}>(${dartLiteral(f.column)}, ${f.codec}, ' + 'nullable: ${f.nullable}, generated: ${f.generated}${f.defaultSql == null ? '' : ', defaultSql: ${dartLiteral(f.defaultSql!)}'}${f.clientDefault == null ? '' : ', clientDefault: ${f.clientDefault}'}${f.computed == null ? '' : ', computed: ${_computedLiteral(f.computed!)}'}${f.integerBits == null || f.integerBits == 64 ? '' : ', integerBits: ${f.integerBits}'}${f.temporalPrecision == null || f.temporalPrecision == 6 ? '' : ', temporalPrecision: ${f.temporalPrecision}'}${f.decimalPrecision == null ? '' : ', decimalPrecision: ${f.decimalPrecision}'}${f.decimalScale == null || f.decimalScale == 0 ? '' : ', decimalScale: ${f.decimalScale}'});', ); } b.writeln( - 'final ${entity.name}Schema = TableSchema(${_literal(entity.table)}, ' - 'columns: [${entity.fields.map((f) => _columnSymbol(entity, f)).join(', ')}], ' - 'primaryKey: ${_strings(entity.columns(entity.primaryKey))}, ' - 'uniqueKeys: [${entity.uniqueKeys.map((k) => _strings(entity.columns(k))).join(', ')}], ' - 'indexes: [${entity.indexes.map((i) => 'IndexSchema(${_literal(i.name)}, ${_strings(entity.columns(i.keys))}, unique: ${i.unique})').join(', ')}], ' - '${entity.checks.isEmpty ? '' : 'checks: [${entity.checks.map((c) => 'CheckSchema.forDialects(${c.name == null ? 'null' : _literal(c.name!)}, sqlite: ${_literal(c.sqlite)}, postgres: ${_literal(c.postgres)}, mysql: ${c.mysql == null ? 'null' : _literal(c.mysql!)}, mariadb: ${c.mariadb == null ? 'null' : _literal(c.mariadb!)})').join(', ')}], '}' - 'foreignKeys: [${entity.edges.where((e) => e.isForeignKey).map((e) => 'ForeignKey(${_strings(entity.columns(e.parentKeys))}, ${_literal(e.target.table)}, ${_strings(e.target.columns(e.childKeys))}, onDelete: ${_literal(e.onDelete!)})').join(', ')}]);', + 'final ${entity.name}Schema = TableSchema(${dartLiteral(entity.table)}, ' + 'columns: [${entity.fields.map((f) => columnSymbol(entity, f)).join(', ')}], ' + 'primaryKey: ${dartStringList(entity.columns(entity.primaryKey))}, ' + 'uniqueKeys: [${entity.uniqueKeys.map((k) => dartStringList(entity.columns(k))).join(', ')}], ' + 'indexes: [${entity.indexes.map((i) => 'IndexSchema(${dartLiteral(i.name)}, ${dartStringList(entity.columns(i.keys))}, unique: ${i.unique})').join(', ')}], ' + '${entity.checks.isEmpty ? '' : 'checks: [${entity.checks.map((c) => 'CheckSchema.forDialects(${c.name == null ? 'null' : dartLiteral(c.name!)}, sqlite: ${dartLiteral(c.sqlite)}, postgres: ${dartLiteral(c.postgres)}, mysql: ${c.mysql == null ? 'null' : dartLiteral(c.mysql!)}, mariadb: ${c.mariadb == null ? 'null' : dartLiteral(c.mariadb!)})').join(', ')}], '}' + 'foreignKeys: [${entity.edges.where((e) => e.isForeignKey).map((e) => 'ForeignKey(${dartStringList(entity.columns(e.parentKeys))}, ${dartLiteral(e.target.table)}, ${dartStringList(e.target.columns(e.childKeys))}, onDelete: ${dartLiteral(e.onDelete!)})').join(', ')}]);', ); b.writeln( 'final class ${entity.fieldsType} extends Fields {\n ${entity.fieldsType}(super.table);', ); for (final f in entity.fields) { b.writeln( - 'late final ${f.name} = ${f.computed == null ? 'column' : 'readColumn'}(${_columnSymbol(entity, f)});', + 'late final ${f.name} = ${f.computed == null ? 'column' : 'readColumn'}(${columnSymbol(entity, f)});', ); } for (final edge in entity.edges) { @@ -110,10 +113,10 @@ String _emit(List<_Entity> schema, String import, _DartNames names) { return b.toString(); } -String _modelSelection(_Entity entity, String row) { +String _modelSelection(ModelEntity entity, String row) { final named = entity.constructorNamedFields; - if (named == null) return _recordSelection(entity.fields, row); - String construct(String Function(_Field) value) => + if (named == null) return recordSelection(entity.fields, row); + String construct(String Function(ModelField) value) => '${entity.rowType}(${entity.fields.map((f) => '${named.contains(f.name) ? '${f.name}: ' : ''}${value(f)}').join(', ')})'; final fields = entity.fields; if (fields.length == 1) { @@ -126,12 +129,12 @@ String _modelSelection(_Entity entity, String row) { } final left = fields.take(5).toList(), right = fields.skip(5).toList(); final leftNames = left.map((f) => f.name).toSet(); - return '(${_recordSelection(left, row)}, ${_recordSelection(right, row)})' + return '(${recordSelection(left, row)}, ${recordSelection(right, row)})' '.map((left, right) => ' '${construct((f) => '${leftNames.contains(f.name) ? 'left' : 'right'}.${f.name}')})'; } -String _recordSelection(List<_Field> fields, String row) { +String recordSelection(List fields, String row) { if (fields.length == 1) { final f = fields.single; return '$row.${f.name}.map((v) => (${f.name}: v,))'; @@ -143,9 +146,9 @@ String _recordSelection(List<_Field> fields, String row) { } // Nested typed composition avoids a combinatorial number of arity helpers. final left = fields.take(5).toList(), right = fields.skip(5).toList(); - return '(${_recordSelection(left, row)}, ${_recordSelection(right, row)})' + return '(${recordSelection(left, row)}, ${recordSelection(right, row)})' '.map((left, right) => (${[...left.map((f) => '${f.name}: left.${f.name}'), ...right.map((f) => '${f.name}: right.${f.name}')].join(', ')}))'; } String _computedLiteral(ComputedColumn value) => - 'ComputedColumn.forDialects(sqlite: ${_literal(value.sqlite)}, postgres: ${_literal(value.postgres)}, mysql: ${value.mysql == null ? 'null' : _literal(value.mysql!)}, mariadb: ${value.mariadb == null ? 'null' : _literal(value.mariadb!)}, storage: ComputedStorage.${value.storage.name})'; + 'ComputedColumn.forDialects(sqlite: ${dartLiteral(value.sqlite)}, postgres: ${dartLiteral(value.postgres)}, mysql: ${value.mysql == null ? 'null' : dartLiteral(value.mysql!)}, mariadb: ${value.mariadb == null ? 'null' : dartLiteral(value.mariadb!)}, storage: ComputedStorage.${value.storage.name})'; diff --git a/lib/src/generate/exception.dart b/lib/src/generate/exception.dart new file mode 100644 index 00000000..c8021ae0 --- /dev/null +++ b/lib/src/generate/exception.dart @@ -0,0 +1,11 @@ +/// A declaration, generated file or migration source that cannot be processed. +final class GenerationException implements Exception { + /// The validation failure, including source location when available. + final String message; + + /// Creates a source-generation failure with an actionable [message]. + const GenerationException(this.message); + + @override + String toString() => 'GenerationException: $message'; +} diff --git a/lib/src/generate/import.dart b/lib/src/generate/import.dart index e8dd4276..1e25b7c5 100644 --- a/lib/src/generate/import.dart +++ b/lib/src/generate/import.dart @@ -1,19 +1,38 @@ -part of '../generate.dart'; +import 'dart:convert'; + +import 'package:analyzer/dart/ast/token.dart' show Keyword; +import 'package:dart_style/dart_style.dart'; + +import '../../migrate.dart'; +import '../../runtime.dart'; +import 'model.dart'; +import 'source.dart'; /// A catalog fact that needs manual handling. Blocking issues prevent a faithful /// declaration of the affected table or relationship; other issues retain objects /// which must stay in reviewed, database-specific migrations. final class SchemaImportIssue { + /// Stable issue category for programmatic handling. final String code; + + /// Physical database object affected by this issue. final String object; + + /// Database fact that needs review or cannot be represented faithfully. final String detail; + + /// Whether the affected declaration or relationship must be omitted. final bool blocking; + + /// Describes an import limitation for one physical [object]. const SchemaImportIssue( this.code, this.object, this.detail, { this.blocking = true, }); + + /// Machine-readable report fields; not an executable schema definition. Map toJson() => { 'code': code, 'object': object, @@ -25,11 +44,22 @@ final class SchemaImportIssue { /// Editable declarations, physical-to-Dart names, and a separate review report. /// This is not a migration or permission to replace the existing schema. final class ImportedSchema { + /// Engine whose physical catalog was inspected. final SqlDialect dialect; + + /// Physical database schema or namespace containing the imported objects. final String schema; + + /// Editable Dart model declarations for supported catalog objects. final String dart; + + /// Physical table names mapped to their generated Dart entity names. final Map entities; + + /// Physical table and column names mapped to Dart field names. final Map> fields; + + /// Catalog features that need review or prevented faithful import. final List issues; ImportedSchema._( this.dialect, @@ -43,7 +73,11 @@ final class ImportedSchema { fields.map((k, v) => MapEntry(k, Map.unmodifiable(v))), ), issues = List.unmodifiable(issues); + + /// Whether any unsupported object prevented a faithful declaration. bool get hasBlockingIssues => issues.any((i) => i.blocking); + + /// Machine-readable names and review issues, excluding generated Dart source. Map toJson() => { 'format': 1, 'dialect': dialect.name, @@ -102,7 +136,7 @@ Future _importCatalog( ? 'main' : (await db.execute( SqlCommand( - _mysqlDialect(db.dialect) + isMysqlDialect(db.dialect) ? 'SELECT DATABASE()' : 'SELECT current_schema()', ), @@ -131,7 +165,7 @@ Future _importCatalog( SqlCommand( db.dialect == SqlDialect.sqlite ? "SELECT name, type, sql FROM main.sqlite_schema WHERE type IN ('table', 'view') AND substr(name, 1, 7) <> 'sqlite_' ORDER BY name" - : _mysqlDialect(db.dialect) + : isMysqlDialect(db.dialect) ? "SELECT TABLE_NAME, TABLE_TYPE, '' FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME" : '''SELECT c.relname, c.relkind::text, CASE WHEN c.relkind IN ('v', 'm') THEN pg_get_viewdef(c.oid) ELSE '' END @@ -139,7 +173,7 @@ Future _importCatalog( WHERE n.nspname = current_schema() AND c.relkind IN ('r', 'p', 'v', 'm', 'f') ORDER BY c.relname''', ), ); - final inventoryRows = _mysqlDialect(db.dialect) + final inventoryRows = isMysqlDialect(db.dialect) ? [ for (final row in inventory.rows) [ @@ -244,7 +278,7 @@ Future _importCatalog( object, extra.definition, blocking: - _mysqlDialect(db.dialect) && + isMysqlDialect(db.dialect) && {'column', 'table options'}.contains(extra.kind), ), ); @@ -282,7 +316,7 @@ Future _importCatalog( ), ); } - if (_mysqlDialect(db.dialect) && + if (isMysqlDialect(db.dialect) && column.storageType.startsWith('DATETIME')) { issues.add( SchemaImportIssue( @@ -293,7 +327,7 @@ Future _importCatalog( ), ); } - if (_mysqlDialect(db.dialect) && column.storageType == 'TINYINT(1)') { + if (isMysqlDialect(db.dialect) && column.storageType == 'TINYINT(1)') { issues.add( SchemaImportIssue( 'IMPORT.BOOLEAN_SEMANTICS', @@ -313,17 +347,17 @@ Future _importCatalog( ); } if (column.generated && column.computed == null) { - if (_mysqlDialect(db.dialect) && + if (isMysqlDialect(db.dialect) && {'SMALLINT', 'INT', 'BIGINT'}.contains(column.storageType) && !column.nullable && - _same(info.primaryKey, [column.name])) { + sameStrings(info.primaryKey, [column.name])) { identities.add(column.name); } else if (db.dialect == SqlDialect.postgres && modes[column.name]![1] == 'd' && modes[column.name]![2] == '' && {'SMALLINT', 'INTEGER', 'BIGINT'}.contains(column.storageType) && !column.nullable && - _same(info.primaryKey, [column.name])) { + sameStrings(info.primaryKey, [column.name])) { identities.add(column.name); } else { issues.add( @@ -371,7 +405,7 @@ Future _importCatalog( // Match physical types exactly. In particular, NUMERIC does not prove BigInt, // and SQLite TEXT does not prove an application DateTime, enum, or JSON codec. (String, String?)? _importType(ColumnInfo column, SqlDialect dialect) { - if (_mysqlDialect(dialect)) return _importMysqlType(column); + if (isMysqlDialect(dialect)) return _importMysqlType(column); return switch (( dialect, column.temporalPrecision == null @@ -450,7 +484,7 @@ String _importCap(String name) => name[0].toUpperCase() + name.substring(1); final class _ImportNames { final used = { ...Keyword.keywords.keys, - ..._databaseMembers, + ...databaseMembers, 'column', 'readColumn', 'appSchema', @@ -561,7 +595,7 @@ ImportedSchema _importDeclarations( b.writeln('\nfinal class ${_importCap(entity)}Row({'); for (final c in info.columns) { final type = _importType(c, dialect)!; - b.writeln('@ColumnName(${_literal(c.name)})'); + b.writeln('@ColumnName(${dartLiteral(c.name)})'); if (c.integerBits != null && c.integerBits != 64) { b.writeln('@IntegerBits(${c.integerBits})'); } @@ -575,12 +609,12 @@ ImportedSchema _importDeclarations( } if (generated[info.name]!.contains(c.name)) b.writeln('@Id.generated()'); if (c.declarationDefaultSql != null) { - b.writeln('@Default.sql(${_literal(c.declarationDefaultSql!)})'); + b.writeln('@Default.sql(${dartLiteral(c.declarationDefaultSql!)})'); } if (c.computed != null) { final computed = c.declarationComputed!; b.writeln( - '@Computed.sql(${_literal(computed.expression(dialect))}, storage: ComputedStorage.${computed.storage.name})', + '@Computed.sql(${dartLiteral(computed.expression(dialect))}, storage: ComputedStorage.${computed.storage.name})', ); issues.add( SchemaImportIssue( @@ -597,7 +631,7 @@ ImportedSchema _importDeclarations( ); } b.writeln( - '});\nfinal $entity = entity<${_importCap(entity)}Row>(table: ${_literal(info.name)});', + '});\nfinal $entity = entity<${_importCap(entity)}Row>(table: ${dartLiteral(info.name)});', ); } String selector(String table, List columns) { @@ -628,7 +662,7 @@ ImportedSchema _importDeclarations( constraint( 'index', index.columns, - extra: ', name: ${_literal(index.name)}, unique: ${index.unique}', + extra: ', name: ${dartLiteral(index.name)}, unique: ${index.unique}', ); } final checks = info.checks.toList() @@ -640,8 +674,8 @@ ImportedSchema _importDeclarations( for (final check in checks) { final symbol = names.take(check.name ?? '${entity}Check'); b.writeln( - 'final $symbol = $entity.check(${_literal(check.expression)}, ' - 'name: ${check.name == null ? 'null' : _literal(check.name!)});', + 'final $symbol = $entity.check(${dartLiteral(check.expression)}, ' + 'name: ${check.name == null ? 'null' : dartLiteral(check.name!)});', ); } if (checks.isNotEmpty) { @@ -669,7 +703,7 @@ ImportedSchema _importDeclarations( ...target.uniqueKeys, for (final i in target.indexes) if (i.unique) i.columns, - ].any((k) => _same(k, key.targetColumns)) || + ].any((k) => sameStrings(k, key.targetColumns)) || key.columns.asMap().entries.any( (entry) => _importType( @@ -710,7 +744,7 @@ ImportedSchema _importDeclarations( fieldNames[info.name]!.used.add(relation); final inverse = fieldNames[key.target]!.take('${entity}Rows'); b.writeln( - 'final $relation = $entity.key(${selector(info.name, key.columns)}).references(${entities[key.target]}.key(${selector(key.target, key.targetColumns)}), inverse: ${_literal(inverse)}, onDelete: .$action);', + 'final $relation = $entity.key(${selector(info.name, key.columns)}).references(${entities[key.target]}.key(${selector(key.target, key.targetColumns)}), inverse: ${dartLiteral(inverse)}, onDelete: .$action);', ); } } diff --git a/lib/src/generate/migrations.dart b/lib/src/generate/migrations.dart index 77a274ea..8e7a3f6f 100644 --- a/lib/src/generate/migrations.dart +++ b/lib/src/generate/migrations.dart @@ -1,4 +1,12 @@ -part of '../generate.dart'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:path/path.dart' as p; + +import '../../migrate.dart'; +import 'exception.dart'; +import 'source.dart'; /// Regenerates only static imports. Fingerprints stay in the migration files. Future writeMigrationRegistry( @@ -12,7 +20,7 @@ Future writeMigrationRegistry( final path = p.join(directory, 'migrations.g.dart'); await _replaceSource( path, - _formatMigration(migrationHistorySource(ids, dialect: dialect)), + formatMigration(migrationHistorySource(ids, dialect: dialect)), ); return path; } @@ -30,8 +38,8 @@ Future writeMigration( final root = Directory(directory); await root.create(recursive: true); await _checkMigrationFiles(root, previous); - final source = _formatMigration(migrationSource(migration)); - final registry = _formatMigration( + final source = formatMigration(migrationSource(migration)); + final registry = formatMigration( migrationHistorySource(all.map((m) => m.id), dialect: history.dialect), ); final file = File(p.join(directory, 'm${migration.id}.dart')); @@ -94,7 +102,7 @@ Future recordMigration( content.replaceRange( literal.offset, literal.end, - _literal(migrations.last.checksum), + dartLiteral(migrations.last.checksum), ), ); return migrations.last.checksum; @@ -122,17 +130,13 @@ Future _checkMigrationFiles( ) async { final actual = await _migrationIds(directory); final expected = migrations.map((m) => m.id).toList(); - if (!_same(actual, expected)) { + if (!sameStrings(actual, expected)) { throw const GenerationException( 'Migration registry is stale. Regenerate static imports, then run the command again.', ); } } -String _formatMigration(String source) => - DartFormatter(languageVersion: DartFormatter.latestLanguageVersion) - .format(source); - Future _replaceSource(String path, String source) async { final kind = await FileSystemEntity.type(path, followLinks: false); if (kind != FileSystemEntityType.notFound && diff --git a/lib/src/generate/model.dart b/lib/src/generate/model.dart index 51fcc414..79981903 100644 --- a/lib/src/generate/model.dart +++ b/lib/src/generate/model.dart @@ -1,10 +1,11 @@ -part of '../generate.dart'; +import '../../schema_model.dart'; +import 'exception.dart'; -bool _mysqlDialect(SqlDialect dialect) => +bool isMysqlDialect(SqlDialect dialect) => dialect == SqlDialect.mysql || dialect == SqlDialect.mariadb; // A Database instance member takes precedence over a generated extension getter. -const _databaseMembers = { +const databaseMembers = { 'sql', 'driver', 'onQuery', @@ -37,7 +38,7 @@ const _databaseMembers = { 'toString', }; -final class _Field { +final class ModelField { final String name; final String column; final String type; @@ -54,7 +55,7 @@ final class _Field { final int? decimalPrecision; final int? decimalScale; final int? temporalPrecision; - const _Field({ + const ModelField({ required this.name, required this.column, required this.type, @@ -103,21 +104,21 @@ final class _Field { } } -final class _Entity { +final class ModelEntity { final String name; final String table; final String row; - final List<_Field> fields; + final List fields; /// Null for structural Record rows; otherwise the primary constructor's /// named parameters. Positional parameters retain declaration order. final Set? constructorNamedFields; List primaryKey; final List> uniqueKeys; - final List<_Index> indexes = []; - final List<_Edge> edges = []; + final List indexes = []; + final List edges = []; final List checks = []; - _Entity( + ModelEntity( this.name, this.table, this.row, @@ -135,7 +136,7 @@ final class _Entity { String get fieldsType => '${symbol}Fields'; String get setType => '${symbol}TableSet'; String get rowType => 'models.$row'; - _Field field(String name) => fields.firstWhere( + ModelField field(String name) => fields.firstWhere( (f) => f.name == name, orElse: () => throw GenerationException('$this has no field $name.'), ); @@ -167,15 +168,15 @@ final class _Entity { String toString() => name; } -final class _Index( +final class ModelIndex( final String name, final List keys, final bool unique, ); -final class _Edge( +final class ModelRelation( final String name, - final _Entity target, + final ModelEntity target, final List parentKeys, final List childKeys, final String? onDelete, { @@ -184,13 +185,5 @@ final class _Edge( bool get isForeignKey => !inverse && onDelete != null; } -String _snake(String value) => value - .replaceAllMapped(RegExp(r'([a-z0-9])([A-Z])'), (m) => '${m[1]}_${m[2]}') - .toLowerCase(); -String _literal(String value) => jsonEncode(value).replaceAll(r'$', r'\$'); -String _strings(List values) => '[${values.map(_literal).join(', ')}]'; -String _columnSymbol(_Entity entity, _Field field) => +String columnSymbol(ModelEntity entity, ModelField field) => '_${entity.name}${field.name[0].toUpperCase()}${field.name.substring(1)}'; -bool _same(List a, List b) => - a.length == b.length && - [for (var i = 0; i < a.length; i++) a[i] == b[i]].every((v) => v); diff --git a/lib/src/generate/queries.dart b/lib/src/generate/queries.dart index ee2c676f..4b48f88c 100644 --- a/lib/src/generate/queries.dart +++ b/lib/src/generate/queries.dart @@ -1,13 +1,41 @@ -part of '../generate.dart'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:path/path.dart' as p; + +import '../../runtime.dart'; +import '../../sql.dart'; +import 'emitter.dart' show recordSelection; +import 'exception.dart'; +import 'model.dart'; +import 'reader.dart'; +import 'source.dart'; +import 'types.dart'; /// Offline generated code and its fixed SQL/codec contracts. Native checks are /// explicit: generation does not claim that a database has accepted the SQL. final class GeneratedQueries { + /// Formatted Dart source for the typed query bindings. final String dart; + + /// Resolved SQL and codec metadata used by database validation. + /// + /// This is an in-memory report, not a persisted schema or migration format. final Map manifest; + + /// Combines generated [dart] source with its validation [manifest]. const GeneratedQueries(this.dart, this.manifest); } +/// Analyzes named SQL declarations and reads their SQL files without writing. +/// +/// [outputPath] determines relative Dart imports and defaults to the source +/// basename with a `.queries.dart` extension. Generation validates declarations; +/// use [checkSqlQueries] to ask a selected database to validate the SQL itself. Future generateQueries( String sourcePath, { String? outputPath, @@ -36,7 +64,7 @@ Future generateQueries( if (errors.isNotEmpty) { throw GenerationException(errors.map((e) => e.toString()).join('\n')); } - return await _generateQueries( + return await generateResolvedQueries( resolved.unit, resolved.libraryElement, p.relative(source, from: p.dirname(output)).replaceAll(r'\', '/'), @@ -52,6 +80,9 @@ Future generateQueries( } } +/// Replaces the generated query bindings only after source validation succeeds. +/// +/// [output] defaults to [source] with a `.queries.dart` extension. Future writeGeneratedQueries(String source, {String? output}) async { output ??= p.setExtension(source, '.queries.dart'); final result = await generateQueries(source, outputPath: output); @@ -60,7 +91,10 @@ Future writeGeneratedQueries(String source, {String? output}) async { await file.writeAsString(result.dart); } -/// Re-analysis prevents checking stale SQL, parameter codecs or generated code. +/// Regenerates [source] in memory and rejects a stale generated [output] file. +/// +/// Returns the current SQL and codec metadata for [checkSqlQueries]. Throws +/// [GenerationException] when the saved bindings differ from current sources. Future checkGeneratedQueries( String source, { String? output, @@ -76,20 +110,20 @@ Future checkGeneratedQueries( } final class _NamedQuery( - final _Entity result, - final List<_Field> parameters, + final ModelEntity result, + final List parameters, final Map sql, ); -Future _generateQueries( +Future generateResolvedQueries( CompilationUnit unit, LibraryElement library, String sourceImport, String Function(Uri) importUri, Future Function(String) readSql, ) async { - final names = _DartNames(library.uri, importUri); - final reader = _SchemaReader(unit, library.typeSystem, names); + final names = DartNames(library.uri, importUri); + final reader = SchemaReader(unit, library.typeSystem, names); final aliases = { for (final alias in unit.declarations.whereType()) alias.name.lexeme: alias, @@ -108,7 +142,10 @@ Future _generateQueries( return alias.type as RecordTypeAnnotation; } - List<_Field> fields(RecordTypeAnnotation record, {required bool parameters}) { + List fields( + RecordTypeAnnotation record, { + required bool parameters, + }) { if (record.positionalFields.isNotEmpty) { throw const GenerationException( 'Named SQL records require named fields.', @@ -117,7 +154,7 @@ Future _generateQueries( final fields = [ for (final f in record.namedFields?.fields ?? []) - reader._readField(f, f.name.lexeme, f.type.type, f.metadata), + reader.readField(f, f.name.lexeme, f.type.type, f.metadata), ]; for (final f in fields) { if (f.id || @@ -133,7 +170,7 @@ Future _generateQueries( 'Query fields accept codec and column-name annotations only.', ); } - if (parameters && f.column != _snake(f.name)) { + if (parameters && f.column != snakeCase(f.name)) { throw const GenerationException( 'SQL parameters use Dart field names, without ColumnName.', ); @@ -156,7 +193,7 @@ Future _generateQueries( if (call is! MethodInvocation || call.methodName.name != 'sqlQuery' || call.methodName.element?.library?.uri.toString() != - 'package:orm/schema.dart') { + schemaDeclarationUri) { continue; } final types = call.typeArguments?.arguments; @@ -171,7 +208,7 @@ Future _generateQueries( if (name.startsWith('_')) { throw const GenerationException('Named queries must be public.'); } - if (_databaseMembers.contains(name)) { + if (databaseMembers.contains(name)) { throw GenerationException('$name conflicts with a Database member.'); } if (types.first.toSource().startsWith('_')) { @@ -183,7 +220,7 @@ Future _generateQueries( final parameterFields = fields(record(types.last), parameters: true); final sql = {}; for (final dialect in SqlDialect.values) { - final path = reader._namedString(call, dialect.name); + final path = reader.namedString(call, dialect.name); if (path == null) continue; if (path.isEmpty || p.url.isAbsolute(path) || @@ -208,14 +245,14 @@ Future _generateQueries( if (sql.isEmpty) { throw GenerationException('$name needs at least one SQL file.'); } - if (queries.any((q) => _snake(q.result.name) == _snake(name))) { + if (queries.any((q) => snakeCase(q.result.name) == snakeCase(name))) { throw GenerationException('Conflicting query name $name.'); } queries.add( _NamedQuery( - _Entity( + ModelEntity( name, - '_orm_sql_${_snake(name)}', + '_orm_sql_${snakeCase(name)}', types.first.toSource(), resultFields, ), @@ -261,17 +298,17 @@ Future _generateQueries( String _emitQueries( List<_NamedQuery> queries, String sourceImport, - _DartNames names, + DartNames names, ) { final b = StringBuffer('// GENERATED CODE - DO NOT MODIFY BY HAND.\n\n') ..writeln("import 'package:orm/sql.dart';") - ..writeln('import ${_literal(sourceImport)} as models;') + ..writeln('import ${dartLiteral(sourceImport)} as models;') ..writeln( - 'export ${_literal(sourceImport)} show ${queries.map((q) => q.result.row).toSet().join(', ')};', + 'export ${dartLiteral(sourceImport)} show ${queries.map((q) => q.result.row).toSet().join(', ')};', ); if (names.typedData) b.writeln("import 'dart:typed_data';"); for (final (uri, prefix) in names.imports) { - b.writeln('import ${_literal(uri)} as $prefix;'); + b.writeln('import ${dartLiteral(uri)} as $prefix;'); } if (queries.any((q) => q.parameters.isNotEmpty)) { b.writeln( @@ -282,7 +319,7 @@ String _emitQueries( final entity = query.result; for (final f in entity.fields) { b.writeln( - 'final ${_queryColumn(entity, f)} = Column<${f.type}>(${_literal(f.column)}, ${f.codec}, nullable: ${f.nullable});', + 'final ${_queryColumn(entity, f)} = Column<${f.type}>(${dartLiteral(f.column)}, ${f.codec}, nullable: ${f.nullable});', ); } b.writeln( @@ -294,9 +331,9 @@ String _emitQueries( b.writeln('}'); b.writeln( 'final _sqlDefinition${entity.symbol} = SqlQueryDefinition<${entity.rowType}, ${entity.fieldsType}>(' - 'Table(TableSchema(${_literal(entity.table)}, columns: [${entity.fields.map((f) => _queryColumn(entity, f)).join(', ')}]), ' - '${entity.fieldsType}.new, (row) => ${_recordSelection(entity.fields, 'row')}), {' - '${query.sql.entries.map((entry) => 'SqlDialect.${entry.key.name}: SqlTemplate(${_literal(entry.value)}, dialect: SqlDialect.${entry.key.name})').join(', ')}' + 'Table(TableSchema(${dartLiteral(entity.table)}, columns: [${entity.fields.map((f) => _queryColumn(entity, f)).join(', ')}]), ' + '${entity.fieldsType}.new, (row) => ${recordSelection(entity.fields, 'row')}), {' + '${query.sql.entries.map((entry) => 'SqlDialect.${entry.key.name}: SqlTemplate(${dartLiteral(entry.value)}, dialect: SqlDialect.${entry.key.name})').join(', ')}' '});', ); b.writeln( @@ -304,14 +341,14 @@ String _emitQueries( 'Query<${entity.rowType}, ${entity.fieldsType}> ${entity.name}(' '${query.parameters.isEmpty ? '' : '{${query.parameters.map((f) => '${f.nullable ? '' : 'required '}${f.type} ${f.name}').join(', ')}}'}' ') => _sqlDefinition${entity.symbol}.bind(this, {' - '${query.parameters.map((f) => '${_literal(f.name)}: _bindSqlParameter(${f.name}, ${f.codec})').join(', ')}' + '${query.parameters.map((f) => '${dartLiteral(f.name)}: _bindSqlParameter(${f.name}, ${f.codec})').join(', ')}' '}); }', ); } return b.toString(); } -String _queryColumn(_Entity entity, _Field field) => +String _queryColumn(ModelEntity entity, ModelField field) => '_sqlColumn${entity.symbol}_${entity.fields.indexOf(field)}'; var _queryCheckSerial = 0; @@ -343,7 +380,7 @@ Future>> checkSqlQueries( ), }, ); - String quote(String name) => _mysqlDialect(db.dialect) + String quote(String name) => isMysqlDialect(db.dialect) ? '`${name.replaceAll('`', '``')}`' : '"${name.replaceAll('"', '""')}"'; final alias = quote('_orm_check'); @@ -352,7 +389,7 @@ Future>> checkSqlQueries( 'FROM (\n${command.sql}\n) AS $alias'; List? nativeTypes; try { - if (_mysqlDialect(db.dialect)) { + if (isMysqlDialect(db.dialect)) { final name = '_orm_check_${DateTime.now().microsecondsSinceEpoch}_${_queryCheckSerial++}'; var prepared = false; diff --git a/lib/src/generate/reader.dart b/lib/src/generate/reader.dart index b682a446..4de93d47 100644 --- a/lib/src/generate/reader.dart +++ b/lib/src/generate/reader.dart @@ -1,13 +1,22 @@ -part of '../generate.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/dart/element/type_system.dart'; -final class _SchemaReader( +import '../../schema_model.dart'; +import 'exception.dart'; +import 'model.dart'; +import 'source.dart'; +import 'types.dart'; + +final class SchemaReader( final CompilationUnit unit, final TypeSystem typeSystem, - final _DartNames names, + final DartNames names, ) { - final Map entities = {}; + final Map entities = {}; - List<_Entity> read() { + List read() { final aliases = { for (final d in unit.declarations.whereType()) d.name.lexeme: d, @@ -27,7 +36,7 @@ final class _SchemaReader( continue; } if (call.methodName.element?.library?.uri.toString() != - 'package:orm/schema.dart') { + schemaDeclarationUri) { continue; } final types = call.typeArguments?.arguments; @@ -37,7 +46,7 @@ final class _SchemaReader( final row = types.single.toSource(); final alias = aliases[row]; final declaration = classes[row]; - final List<_Field> fields; + final List fields; final AstNode model; Set? constructorNamedFields; if (declaration != null) { @@ -55,7 +64,7 @@ final class _SchemaReader( } fields = [ for (final f in record.namedFields!.fields) - _readField(f, f.name.lexeme, f.type.type, f.metadata), + readField(f, f.name.lexeme, f.type.type, f.metadata), ]; } else { _fail( @@ -65,20 +74,20 @@ final class _SchemaReader( } if (fields.isEmpty) _fail(model, 'A model needs fields.'); final name = variable.name.lexeme; - if (name.startsWith('_') || _databaseMembers.contains(name)) { + if (name.startsWith('_') || databaseMembers.contains(name)) { _fail( variable, 'Entity $name must be public and cannot shadow a Database member. Rename the Dart declaration and keep table: for its physical name.', ); } - final table = _namedString(call, 'table') ?? _snake(name); + final table = namedString(call, 'table') ?? snakeCase(name); if (entities.values.any((e) => e.table == table)) { _fail(call, 'Duplicate physical table $table.'); } if (fields.map((f) => f.column).toSet().length != fields.length) { _fail(model, 'Duplicate physical column name.'); } - final entity = _Entity( + final entity = ModelEntity( name, table, row, @@ -107,13 +116,13 @@ final class _SchemaReader( } final name = _named(call, 'name') is NullLiteral ? null - : _namedString(call, 'name') ?? _snake(variable.name.lexeme); + : namedString(call, 'name') ?? snakeCase(variable.name.lexeme); final check = CheckSchema.forDialects( name, - sqlite: _namedString(call, 'sqlite') ?? expression.value, - postgres: _namedString(call, 'postgres') ?? expression.value, - mysql: _namedString(call, 'mysql') ?? expression.value, - mariadb: _namedString(call, 'mariadb') ?? expression.value, + sqlite: namedString(call, 'sqlite') ?? expression.value, + postgres: namedString(call, 'postgres') ?? expression.value, + mysql: namedString(call, 'mysql') ?? expression.value, + mariadb: namedString(call, 'mariadb') ?? expression.value, ); if (SqlDialect.values.every( (d) => check.expression(d).trim().isEmpty, @@ -143,8 +152,8 @@ final class _SchemaReader( entity.uniqueKeys.add(keys); case 'index': entity.indexes.add( - _Index( - _namedString(call, 'name') ?? _snake(variable.name.lexeme), + ModelIndex( + namedString(call, 'name') ?? snakeCase(variable.name.lexeme), keys, _named(call, 'unique')?.toSource() == 'true', ), @@ -156,7 +165,7 @@ final class _SchemaReader( if (call is! MethodInvocation || !{'references', 'relatesTo'}.contains(call.methodName.name) || call.methodName.element?.library?.uri.toString() != - 'package:orm/schema.dart') { + schemaDeclarationUri) { continue; } final foreignKey = call.methodName.name == 'references'; @@ -177,7 +186,7 @@ final class _SchemaReader( for (final i in target.indexes) if (i.unique) i.keys, ]; - if (foreignKey && !targets.any((key) => _same(key, remote))) { + if (foreignKey && !targets.any((key) => sameStrings(key, remote))) { _fail( call, 'Foreign key target must be a primary or unique key in the same column order.', @@ -208,14 +217,21 @@ final class _SchemaReader( } _addEdge( source, - _Edge(variable.name.lexeme, target, local, remote, onDelete), + ModelRelation(variable.name.lexeme, target, local, remote, onDelete), call, ); - final inverse = _namedString(call, 'inverse'); + final inverse = namedString(call, 'inverse'); if (inverse != null) { _addEdge( target, - _Edge(inverse, source, remote, local, onDelete, inverse: true), + ModelRelation( + inverse, + source, + remote, + local, + onDelete, + inverse: true, + ), call, ); } @@ -229,7 +245,7 @@ final class _SchemaReader( '${entity.symbol}Updates', '${entity.name}Schema', '${entity.name}Table', - for (final field in entity.fields) _columnSymbol(entity, field), + for (final field in entity.fields) columnSymbol(entity, field), ]) { if (!symbols.add(symbol)) { throw GenerationException( @@ -250,7 +266,8 @@ final class _SchemaReader( } } for (final f in entity.fields.where((f) => f.generated)) { - if (f.storage != 'integer' || !_same(entity.primaryKey, [f.name])) { + if (f.storage != 'integer' || + !sameStrings(entity.primaryKey, [f.name])) { throw GenerationException( 'Generated identity requires a single integer primary key: ${entity.name}.${f.name}.', ); @@ -265,7 +282,7 @@ final class _SchemaReader( return entities.values.toList(); } - (List<_Field>, Set) _readClass(ClassDeclaration declaration) { + (List, Set) _readClass(ClassDeclaration declaration) { final primary = declaration.namePart; if (primary is! PrimaryConstructorDeclaration || primary.typeName.lexeme.startsWith('_') || @@ -282,7 +299,7 @@ final class _SchemaReader( 'Use a public final class with an unnamed primary constructor, declaring fields only, and no inheritance or type parameters. Put behavior in extensions.', ); } - final fields = <_Field>[]; + final fields = []; final named = {}; for (final parameter in primary.formalParameters.parameters) { if (!parameter.isFinal || @@ -299,14 +316,14 @@ final class _SchemaReader( } final name = parameter.name!.lexeme; fields.add( - _readField(parameter, name, parameter.type!.type, parameter.metadata), + readField(parameter, name, parameter.type!.type, parameter.metadata), ); if (parameter.isNamed) named.add(name); } return (fields, named); } - _Field _readField( + ModelField readField( AstNode field, String name, DartType? type, @@ -331,7 +348,7 @@ final class _SchemaReader( final annotationType = value?.type; if (annotationType is! InterfaceType || annotationType.element.library.uri.toString() != - 'package:orm/schema.dart') { + schemaDeclarationUri) { continue; } switch (annotationType.element.name) { @@ -434,8 +451,7 @@ final class _SchemaReader( final codecType = reference.staticType; if (codecType is! InterfaceType || codecType.element.name != 'Codec' || - codecType.element.library.uri.toString() != - 'package:orm/values.dart') { + codecType.element.library.uri.toString() != codecLibraryUri) { _fail(custom, 'Use a const Codec.'); } final domain = codecType.typeArguments.single; @@ -473,7 +489,7 @@ final class _SchemaReader( !{ 'dart:core', 'dart:typed_data', - 'package:orm/values.dart', + ...valueLibraryUris, }.contains(type.element.library.uri.toString())) { _fail(field, 'This type needs an explicit @UseCodec.'); } @@ -523,9 +539,9 @@ final class _SchemaReader( 'IntegerBits requires integer storage and a width of 16, 32 or 64.', ); } - return _Field( + return ModelField( name: name, - column: column ?? _snake(name), + column: column ?? snakeCase(name), type: names.type(type), codec: codec, storage: storage, @@ -543,7 +559,7 @@ final class _SchemaReader( ); } - (_Entity, List) _key(Expression expression) { + (ModelEntity, List) _key(Expression expression) { if (expression is! MethodInvocation || expression.methodName.name != 'key' || expression.target is! SimpleIdentifier) { @@ -558,7 +574,7 @@ final class _SchemaReader( ); } - List _selector(Expression expression, _Entity entity) { + List _selector(Expression expression, ModelEntity entity) { if (expression is! FunctionExpression || expression.parameters?.parameters.length != 1 || expression.body is! ExpressionFunctionBody) { @@ -602,7 +618,7 @@ final class _SchemaReader( return keys; } - void _addEdge(_Entity source, _Edge edge, AstNode node) { + void _addEdge(ModelEntity source, ModelRelation edge, AstNode node) { if (!RegExp(r'^[a-zA-Z][a-zA-Z0-9_]*$').hasMatch(edge.name) || source.fields.any((f) => f.name == edge.name) || source.edges.any((e) => e.name == edge.name) || @@ -620,7 +636,7 @@ final class _SchemaReader( return null; } - String? _namedString(MethodInvocation call, String name) { + String? namedString(MethodInvocation call, String name) { final expression = _named(call, name); if (expression == null) return null; if (expression is! SimpleStringLiteral) { diff --git a/lib/src/generate.dart b/lib/src/generate/schema.dart similarity index 63% rename from lib/src/generate.dart rename to lib/src/generate/schema.dart index 9f8cba32..f8146eb2 100644 --- a/lib/src/generate.dart +++ b/lib/src/generate/schema.dart @@ -1,49 +1,42 @@ -/// Source analysis and deterministic client generation. -library; - -import 'dart:convert'; import 'dart:io'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/dart/analysis/session.dart'; -import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/token.dart' show Keyword; import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/nullability_suffix.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/dart/element/type_system.dart'; -import 'package:build/build.dart' as builder; import 'package:dart_style/dart_style.dart'; import 'package:path/path.dart' as p; -import '../migrate.dart'; -import '../orm.dart'; - -part 'generate/model.dart'; -part 'generate/reader.dart'; -part 'generate/types.dart'; -part 'generate/emitter.dart'; -part 'generate/build.dart'; -part 'generate/import.dart'; -part 'generate/queries.dart'; -part 'generate/migrations.dart'; - -final class GenerationException implements Exception { - final String message; - const GenerationException(this.message); - @override - String toString() => 'GenerationException: $message'; -} +import '../../migrate.dart'; +import 'emitter.dart'; +import 'exception.dart'; +import 'reader.dart'; +import 'source.dart'; +import 'types.dart'; +/// A generated typed client and the physical schema derived from its models. +/// +/// Generation does not open a database or apply a migration. Review schema +/// changes through the migration workflow before applying them. final class GeneratedSchema { + /// Formatted Dart source for the generated query client. final String dart; + + /// Physical tables, keys and constraints captured from the declaration. final SchemaSnapshot snapshot; + + /// Combines generated [dart] source with its physical [snapshot]. const GeneratedSchema(this.dart, this.snapshot); - String get snapshotDart => _formatMigration(schemaSource(snapshot)); + + /// Standalone Dart source for [snapshot], independent of current model types. + String get snapshotDart => formatMigration(schemaSource(snapshot)); } +/// Analyzes [sourcePath] and generates a client without writing files. +/// +/// [outputPath] determines relative imports and defaults to the source basename +/// with an `.orm.dart` extension. Invalid declarations or output collisions throw +/// [GenerationException]. Application default factories are never executed. Future generateSchema( String sourcePath, { String? outputPath, @@ -77,7 +70,7 @@ Future generateSchema( final import = p .relative(source, from: p.dirname(output)) .replaceAll(r'\', '/'); - return _generate( + return generateResolvedSchema( resolved.unit, resolved.libraryElement, import, @@ -92,22 +85,26 @@ Future generateSchema( } } -GeneratedSchema _generate( +GeneratedSchema generateResolvedSchema( CompilationUnit unit, LibraryElement library, String sourceImport, String Function(Uri) importUri, ) { - final names = _DartNames(library.uri, importUri); - final schema = _SchemaReader(unit, library.typeSystem, names).read(); + final names = DartNames(library.uri, importUri); + final schema = SchemaReader(unit, library.typeSystem, names).read(); return GeneratedSchema( DartFormatter(languageVersion: library.languageVersion.effective) - .format(_emit(schema, sourceImport, names)), + .format(emitSchema(schema, sourceImport, names)), SchemaSnapshot([for (final table in schema) table.snapshot()]), ); } -/// Writes generated output only after source analysis and schema validation. +/// Writes a typed client and Dart schema snapshot after validating [source]. +/// +/// [output] defaults to the source basename with an `.orm.dart` extension. +/// The snapshot uses the corresponding `.snapshot.dart` basename. Both are +/// derived files and are replaced when generation succeeds. Future writeGeneratedSchema(String source, {String? output}) async { output ??= p.setExtension(source, '.orm.dart'); final result = await generateSchema(source, outputPath: output); diff --git a/lib/src/generate/source.dart b/lib/src/generate/source.dart new file mode 100644 index 00000000..11b4517f --- /dev/null +++ b/lib/src/generate/source.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:dart_style/dart_style.dart'; + +String snakeCase(String value) => value + .replaceAllMapped(RegExp(r'([a-z0-9])([A-Z])'), (m) => '${m[1]}_${m[2]}') + .toLowerCase(); +String dartLiteral(String value) => jsonEncode(value).replaceAll(r'$', r'\$'); +String dartStringList(List values) => + '[${values.map(dartLiteral).join(', ')}]'; +bool sameStrings(List a, List b) => + a.length == b.length && + [for (var i = 0; i < a.length; i++) a[i] == b[i]].every((v) => v); + +String formatMigration(String source) => + DartFormatter(languageVersion: DartFormatter.latestLanguageVersion) + .format(source); diff --git a/lib/src/generate/types.dart b/lib/src/generate/types.dart index 848534e1..ec4835db 100644 --- a/lib/src/generate/types.dart +++ b/lib/src/generate/types.dart @@ -1,7 +1,23 @@ -part of '../generate.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import 'exception.dart'; +import 'source.dart'; + +// Match defining libraries, not consumer import text. Reexports preserve these +// identities, so user-defined lookalike annotations and codecs are rejected. +const schemaDeclarationUri = 'package:orm/src/schema/declaration.dart'; +const codecLibraryUri = 'package:orm/src/values/codec.dart'; +const valueLibraryUris = { + codecLibraryUri, + 'package:orm/src/values/decimal.dart', + 'package:orm/src/values/temporal.dart', +}; /// Resolve symbols by defining library rather than copying source import text. -final class _DartNames(final Uri source, final String Function(Uri) importUri) { +final class DartNames(final Uri source, final String Function(Uri) importUri) { final Map _prefixes = {}; bool typedData = false; Iterable<(String, String)> get imports sync* { @@ -22,9 +38,8 @@ final class _DartNames(final Uri source, final String Function(Uri) importUri) { if ({ 'dart:core', 'dart:typed_data', - 'package:orm/orm.dart', - 'package:orm/values.dart', - 'package:orm/schema_model.dart', + ...valueLibraryUris, + 'package:orm/src/schema/model.dart', }.contains(uri.toString())) { return element.name!; } @@ -87,7 +102,7 @@ final class _DartNames(final Uri source, final String Function(Uri) importUri) { final type = value?.type; if (type is InterfaceType && type.element.name == 'EnumValue' && - type.element.library.uri.toString() == 'package:orm/schema.dart') { + type.element.library.uri.toString() == schemaDeclarationUri) { if (label != null) { throw GenerationException( 'EnumValue appears twice on ${field.name}.', @@ -106,7 +121,7 @@ final class _DartNames(final Uri source, final String Function(Uri) importUri) { throw GenerationException('An empty enum cannot be stored.'); } final symbol = name(element); - return 'Codecs.enumeration<$symbol>({${values.entries.map((e) => '$symbol.${e.key}: ${_literal(e.value)}').join(', ')}})'; + return 'Codecs.enumeration<$symbol>({${values.entries.map((e) => '$symbol.${e.key}: ${dartLiteral(e.value)}').join(', ')}})'; } String factoryReference( diff --git a/lib/src/instant.dart b/lib/src/instant.dart deleted file mode 100644 index d7a5a3cd..00000000 --- a/lib/src/instant.dart +++ /dev/null @@ -1,123 +0,0 @@ -part of '../values.dart'; - -// Intersection of PostgreSQL's finite lower bound and Dart DateTime's range. -const _minInstantMicros = -210866803200000000; -const _maxInstantMicros = 8640000000000000000; -const _unixEpochJulian = 2440588; -const _maxInstantJulian = 102440588; - -DateTime _decodeInstant(Object? value) => switch (value) { - DateTime() => _checkedInstant(value), - String() => _parseInstant(value), - _ => throw const FormatException('Expected a UTC instant or timestamp text.'), -}; - -DateTime _checkedInstant(DateTime value) { - final utc = value.toUtc(), millis = value.millisecondsSinceEpoch; - // Epoch milliseconds fit JavaScript's exact integer range; total microseconds - // do not. DateTime stores the remaining microsecond component separately. - if (millis < _minInstantMicros ~/ 1000 || - millis > _maxInstantMicros ~/ 1000 || - millis == _maxInstantMicros ~/ 1000 && utc.microsecond != 0) { - throw const FormatException( - 'Instant exceeds the common DateTime/PostgreSQL range.', - ); - } - return utc; -} - -String _encodeInstant(DateTime value) { - final utc = _checkedInstant(value); - final date = LocalDate(utc.year, utc.month, utc.day); - final time = LocalTime( - utc.hour, - utc.minute, - utc.second, - utc.millisecond * 1000 + utc.microsecond, - ); - return '${date._civil} $time+00${date.year <= 0 ? ' BC' : ''}'; -} - -final _instantSyntax = RegExp( - r'^(-?\d{4,7})-(\d{2})-(\d{2})[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?)(Z|[+-]\d{2}(?::?\d{2})?(?::?\d{2})?)?( BC)?$', - caseSensitive: false, -); - -DateTime _parseInstant(String text) { - final match = _instantSyntax.firstMatch(text); - if (match == null || match.end != text.length) { - throw const FormatException( - 'Expected a timestamp with at most six fractional digits.', - ); - } - try { - var year = int.parse(match[1]!); - if (match[6] != null) { - if (year <= 0) { - throw const FormatException('BC requires a positive era year.'); - } - year = 1 - year; - } - final month = int.parse(match[2]!), day = int.parse(match[3]!); - if (month < 1 || - month > 12 || - day < 1 || - day > LocalDate._monthDays(year, month)) { - throw const FormatException('Invalid calendar date in instant.'); - } - final zone = match[5]; - var offset = 0; - if (zone != null && zone.toUpperCase() != 'Z') { - final digits = zone.substring(1).replaceAll(':', ''); - final hours = int.parse(digits.substring(0, 2)); - final minutes = digits.length >= 4 - ? int.parse(digits.substring(2, 4)) - : 0; - final seconds = digits.length == 6 - ? int.parse(digits.substring(4, 6)) - : 0; - if (hours > 15 || minutes > 59 || seconds > 59) { - throw const FormatException('Invalid UTC offset.'); - } - offset = - ((hours * 60 + minutes) * 60 + seconds) * - 1000000 * - (zone[0] == '-' ? -1 : 1); - } - // Zone-less database storage is explicitly UTC (SQLite CURRENT_TIMESTAMP), - // independent of the process's local timezone. Validate before multiplying - // days into int64 microseconds, including offset crossings at either bound. - final time = LocalTime.parse(match[4]!).microseconds - offset; - final ordinal = - _gregorianJulianDay(year, month, day) + - _floorDiv(time, _microsecondsPerDay); - final within = time % _microsecondsPerDay; - if (ordinal < 0 || - ordinal > _maxInstantJulian || - ordinal == _maxInstantJulian && within != 0) { - throw const FormatException( - 'Instant exceeds the common DateTime/PostgreSQL range.', - ); - } - if (const bool.fromEnvironment('dart.library.js_interop')) { - final date = LocalDate.fromJulianDay(ordinal); - final clock = LocalTime.fromMicroseconds(within); - return DateTime.utc( - date.year, - date.month, - date.day, - clock.hour, - clock.minute, - clock.second, - clock.microsecond ~/ 1000, - clock.microsecond % 1000, - ); - } - return DateTime.fromMicrosecondsSinceEpoch( - (ordinal - _unixEpochJulian) * _microsecondsPerDay + within, - isUtc: true, - ); - } on RangeError { - throw const FormatException('Invalid timestamp components.'); - } -} diff --git a/lib/src/migrate/backfill.dart b/lib/src/migrate/backfill.dart index 5daf9e0c..e65e5e5b 100644 --- a/lib/src/migrate/backfill.dart +++ b/lib/src/migrate/backfill.dart @@ -1,123 +1,37 @@ -part of '../../migrate.dart'; +// Bounded data backfills and durable primary-key cursors. -/// A reviewed, bounded update using this migration's historical table definition. -/// Expressions and completion SQL are trusted migration code. Primary keys must -/// remain immutable, including in application writes and triggers. -final class Backfill extends MigrationStep { - final TableSchema table; - final Map set; - final String where; - final String doneWhen; - final int batchSize; - late final List> _keys = [ - for (final name in table.primaryKey) - table.columns.singleWhere((c) => c.name == name), - ]; - - Backfill( - this.table, { - required Map set, - this.where = 'TRUE', - required this.doneWhen, - this.batchSize = 1000, - }) : set = Map.unmodifiable(set) { - SchemaSnapshot([table]); - if (batchSize < 1 || - set.isEmpty || - where.trim().isEmpty || - set.entries.any( - (e) => - e.value.trim().isEmpty || - !table.columns.any( - (c) => c.name == e.key && !c.generated && c.computed == null, - ) || - table.primaryKey.contains(e.key), - )) { - throw ArgumentError( - 'Backfill needs a positive batch size and assignments to existing non-key columns.', - ); - } - if (table.primaryKey.isEmpty || - _keys.any((c) => c.nullable || c.codec.sqlType == 'json')) { - throw const OrmException( - 'MIGRATION.BACKFILL_KEY', - 'Backfill requires a non-null primary key with lossless cursor storage; JSON keys are unsupported.', - ); - } - if (_sqlWords(doneWhen).firstOrNull != 'SELECT') { - throw const OrmException( - 'MIGRATION.PROBE', - 'Backfill completion must be a SELECT returning one boolean.', - ); - } - } +import 'dart:convert' show base64Decode, base64Encode, jsonEncode; - @override - Map toJson() => { - 'kind': 'backfill', - 'table': _tableJson(table), - 'set': set, - 'where': where, - 'doneWhen': doneWhen, - 'batchSize': batchSize, - }; - - List _saveKey(List row) => [ - for (var i = 0; i < _keys.length; i++) - switch (_keys[i].codec.sqlType) { - 'text' => row[i] as String, - 'integer' => (row[i] as int).toString(), - 'bigint' => row[i].toString(), - 'decimal' => Codecs.decimal.decode(row[i]).toString(), - 'instant' => - Codecs.dateTime.encode(Codecs.dateTime.decode(row[i])) as String, - 'date' => Codecs.date.decode(row[i]).toString(), - 'time' => Codecs.time.decode(row[i]).toString(), - 'local_datetime' => Codecs.localDateTime.decode(row[i]).toString(), - 'timestamp' => - row[i] is DateTime - ? (row[i] as DateTime).toUtc().toIso8601String() - : row[i] as String, - 'boolean' => Codecs.boolean.decode(row[i]) ? '1' : '0', - 'blob' => base64Encode((row[i] as List)), - 'real' when (row[i] as num).isFinite => row[i].toString(), - _ => throw const OrmException( - 'MIGRATION.BACKFILL_KEY', - 'Backfill keys must have lossless, finite storage values.', - ), - }, - ]; - List _loadKey(List row, SqlDialect dialect) { - if (row.length != _keys.length) { - throw const OrmException( - 'MIGRATION.CHECKPOINT', - 'Backfill cursor key length differs from the historical primary key.', - ); - } - return [ - for (var i = 0; i < row.length; i++) - switch (_keys[i].codec.sqlType) { - 'integer' => int.parse(row[i]), - 'real' => double.parse(row[i]), - 'instant' when _isMysql(dialect) => Codecs.dateTime.decode(row[i]), - 'timestamp' when dialect == SqlDialect.postgres => DateTime.parse( - row[i], - ), - 'boolean' => - dialect == SqlDialect.postgres ? row[i] == '1' : int.parse(row[i]), - 'blob' => base64Decode(row[i]), - _ => row[i], - }, - ]; - } -} +import '../../driver.dart' show Backend, SqlCommand, SqlDialect, SqlResult; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' show Column; +import '../../values.dart' show Codecs, OrmException; +import 'catalog.dart' show verifySchema; +import 'migration.dart' show Migration; +import 'mysql_schema.dart' show isMysqlFamily, mysqlColumnType; +import 'recovery.dart' show checkpoint, probe; +import 'snapshot.dart' show SchemaSnapshot; +import 'sql_utils.dart' show quoteIdentifier; +import 'step.dart' show Backfill; /// Durable counters and primary-key cursors. Keys use lossless strings (binary /// keys use base64), so JSON transport never rounds 64-bit integer identifiers. final class BackfillProgress { + /// Inclusive primary-key boundary captured when this backfill first scanned. + /// + /// Null means that initial scan found no matching rows. Key components are + /// losslessly encoded strings in the historical primary-key order. final List? upperKey; + + /// Last committed primary key; the next batch resumes strictly after it. + /// + /// Null means no nonempty batch has been recorded yet. final List? lastKey; + + /// Total updated rows recorded across committed batches. final int rows; + + /// Number of nonempty batches committed with their cursor checkpoints. final int batches; BackfillProgress._( List? upper, @@ -155,6 +69,8 @@ final class BackfillProgress { } return value; } + + /// Versioned checkpoint data with lossless string-encoded primary keys. Map toJson() => { 'format': 1, 'upper': upperKey, @@ -164,10 +80,11 @@ final class BackfillProgress { }; } -Future _verifyBackfill(SqlDatabase db, Backfill step) async { - if ((!db.capabilities.returning && !_isMysql(db.dialect)) || +Future verifyBackfill(SqlDatabase db, Backfill step) async { + final cursor = _BackfillCursor(step); + if ((!db.capabilities.returning && !isMysqlFamily(db.dialect)) || db.capabilities.maxParameters < 7 || - db.capabilities.maxParameters < step._keys.length * 2) { + db.capabilities.maxParameters < cursor._keys.length * 2) { throw const OrmException( 'MIGRATION.BACKFILL_CAPABILITY', 'Backfill needs RETURNING and room for both cursor bounds.', @@ -175,7 +92,7 @@ Future _verifyBackfill(SqlDatabase db, Backfill step) async { } final result = await verifySchema(db, SchemaSnapshot([step.table])); if (!result.matches || - (_isMysql(db.dialect) && result.unmanaged.isNotEmpty)) { + (isMysqlFamily(db.dialect) && result.unmanaged.isNotEmpty)) { throw OrmException( 'MIGRATION.DRIFT', [ @@ -213,7 +130,7 @@ WHERE n.nspname = current_schema() AND c.relname = $1''', } } -String _mark(SqlDialect dialect, int n) => switch (dialect) { +String parameterMarker(SqlDialect dialect, int n) => switch (dialect) { SqlDialect.sqlite => '?$n', SqlDialect.postgres => '\$$n', SqlDialect.mysql || SqlDialect.mariadb => '?', @@ -222,18 +139,19 @@ String _tuple(List values) => values.length == 1 ? values.single : '(${values.join(', ')})'; /// Exactly one transaction: selected rows, writes and frontier advance commit together. -Future _backfillChunk( +Future backfillChunk( SqlDatabase tx, Migration migration, int index, Backfill step, BackfillProgress? saved, - _BackfillBudget budget, + BackfillBudget budget, void Function(String) phase, ) async { budget.check(); - final names = step.table.primaryKey.map(_quote).toList(), - table = _quote(step.table.name); + final cursor = _BackfillCursor(step); + final names = step.table.primaryKey.map(quoteIdentifier).toList(), + table = quoteIdentifier(step.table.name); final columns = names.join(', '), key = _tuple(names); phase('scan'); var progress = saved; @@ -244,7 +162,7 @@ Future _backfillChunk( ), ); progress = BackfillProgress._( - upper.rows.isEmpty ? null : step._saveKey(upper.rows.single), + upper.rows.isEmpty ? null : cursor._saveKey(upper.rows.single), null, 0, 0, @@ -253,13 +171,13 @@ Future _backfillChunk( final parameters = []; String bound(List values) { final start = parameters.length + 1; - parameters.addAll(step._loadKey(values, tx.dialect)); + parameters.addAll(cursor._loadKey(values, tx.dialect)); return _tuple([ for (var i = 0; i < values.length; i++) - _isMysql(tx.dialect) && - {'bigint', 'decimal'}.contains(step._keys[i].codec.sqlType) - ? 'CAST(${_mark(tx.dialect, start + i)} AS ${_mysqlColumnType(step._keys[i])})' - : _mark(tx.dialect, start + i), + isMysqlFamily(tx.dialect) && + {'bigint', 'decimal'}.contains(cursor._keys[i].codec.sqlType) + ? 'CAST(${parameterMarker(tx.dialect, start + i)} AS ${mysqlColumnType(cursor._keys[i])})' + : parameterMarker(tx.dialect, start + i), ]); } @@ -280,14 +198,14 @@ Future _backfillChunk( ); if (selected.rows.isEmpty) { phase('verify'); - if (!await _probe(tx, step.doneWhen)) { + if (!await probe(tx, step.doneWhen)) { throw const OrmException( 'MIGRATION.POSTCONDITION', 'Backfill completion condition is false. Inspect the data and checkpoint before resuming.', ); } phase('record'); - await _checkpoint( + await checkpoint( tx, migration, index, @@ -297,7 +215,7 @@ Future _backfillChunk( ); return true; } - final keys = selected.rows.map(step._saveKey).toList(); + final keys = selected.rows.map(cursor._saveKey).toList(); if (keys.map(jsonEncode).toSet().length != keys.length) { throw const OrmException( 'MIGRATION.BACKFILL_KEY', @@ -312,17 +230,17 @@ Future _backfillChunk( phase('update'); final updated = await tx.execute( SqlCommand( - 'UPDATE $table SET ${step.set.entries.map((e) => '${_quote(e.key)} = ${e.value}').join(', ')} WHERE $predicate${_isMysql(tx.dialect) ? '' : ' AND (${step.where}) RETURNING $columns'}', + 'UPDATE $table SET ${step.set.entries.map((e) => '${quoteIdentifier(e.key)} = ${e.value}').join(', ')} WHERE $predicate${isMysqlFamily(tx.dialect) ? '' : ' AND (${step.where}) RETURNING $columns'}', parameters, ), changedTables: [step.table.name], ); final expected = keys.map(jsonEncode).toSet(), - actual = (_isMysql(tx.dialect) ? selected.rows : updated.rows) - .map(step._saveKey) + actual = (isMysqlFamily(tx.dialect) ? selected.rows : updated.rows) + .map(cursor._saveKey) .map(jsonEncode) .toSet(); - if ((!_isMysql(tx.dialect) && updated.rows.length != keys.length) || + if ((!isMysqlFamily(tx.dialect) && updated.rows.length != keys.length) || actual.length != expected.length || !actual.containsAll(expected)) { throw const OrmException( @@ -341,7 +259,7 @@ Future _backfillChunk( ); } phase('record'); - await _checkpoint( + await checkpoint( tx, migration, index, @@ -358,16 +276,77 @@ Future _backfillChunk( return false; } -final class _BackfillPaused implements Exception {} +final class BackfillPaused implements Exception {} -final class _BackfillBudget { +final class BackfillBudget { int? remaining; - _BackfillBudget(this.remaining); + BackfillBudget(this.remaining); void check() { - if (remaining == 0) throw _BackfillPaused(); + if (remaining == 0) throw BackfillPaused(); } void used() { if (remaining != null) remaining = remaining! - 1; } } + +final class _BackfillCursor(final Backfill step) { + late final List> _keys = [ + for (final name in step.table.primaryKey) + step.table.columns.singleWhere((c) => c.name == name), + ]; + + List _saveKey(List row) => [ + for (var i = 0; i < _keys.length; i++) + switch (_keys[i].codec.sqlType) { + 'text' => row[i] as String, + 'integer' => (row[i] as int).toString(), + 'bigint' => row[i].toString(), + 'decimal' => Codecs.decimal.decode(row[i]).toString(), + 'instant' => + Codecs.dateTime.encode(Codecs.dateTime.decode(row[i])) as String, + 'date' => Codecs.date.decode(row[i]).toString(), + 'time' => Codecs.time.decode(row[i]).toString(), + 'local_datetime' => Codecs.localDateTime.decode(row[i]).toString(), + 'timestamp' => + row[i] is DateTime + ? (row[i] as DateTime).toUtc().toIso8601String() + : row[i] as String, + 'boolean' => Codecs.boolean.decode(row[i]) ? '1' : '0', + 'blob' => base64Encode((row[i] as List)), + 'real' when (row[i] as num).isFinite => row[i].toString(), + _ => throw const OrmException( + 'MIGRATION.BACKFILL_KEY', + 'Backfill keys must have lossless, finite storage values.', + ), + }, + ]; + List _loadKey(List row, SqlDialect dialect) { + if (row.length != _keys.length) { + throw const OrmException( + 'MIGRATION.CHECKPOINT', + 'Backfill cursor key length differs from the historical primary key.', + ); + } + return [ + for (var i = 0; i < row.length; i++) + switch (_keys[i].codec.sqlType) { + 'integer' => int.parse(row[i]), + 'real' => double.parse(row[i]), + 'instant' when isMysqlFamily(dialect) => Codecs.dateTime.decode( + row[i], + ), + 'timestamp' when dialect == SqlDialect.postgres => DateTime.parse( + row[i], + ), + 'boolean' => + dialect == SqlDialect.postgres ? row[i] == '1' : int.parse(row[i]), + 'blob' => base64Decode(row[i]), + _ => row[i], + }, + ]; + } +} + +BackfillProgress readBackfillProgress(Object? data) => + BackfillProgress._read(data); diff --git a/lib/src/migrate/catalog.dart b/lib/src/migrate/catalog.dart index ffff1b34..04590706 100644 --- a/lib/src/migrate/catalog.dart +++ b/lib/src/migrate/catalog.dart @@ -1,21 +1,76 @@ -part of '../../migrate.dart'; +import 'dart:convert' show jsonEncode; +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' show Column, ForeignKey, IndexSchema; +import '../../values.dart' + show Codecs, Decimal, InstantPrecision, LocalDate, LocalDateTime, LocalTime; +import 'checks.dart' show CheckInfo, matchChecks; +import 'columns.dart' + show ColumnInfo, inspectColumns, matchesCollation, matchesDecimalDigits; +import 'computed.dart' show verifyComputed, withoutComputed; +import 'mysql_catalog.dart' show mysqlTable, mysqlVerifySchema; +import 'mysql_schema.dart' show isMysqlFamily; +import 'schema.dart' show coerceColumn, columnStorageType; +import 'snapshot.dart' show SchemaSnapshot, foreignKeyJson, indexJson; +import 'sql_utils.dart' + show canonicalMigrationValue, migrationHash, quoteIdentifier; +import 'sqlite_checks.dart' + show + sqliteChecks, + uncoerceDecimalDefault, + uncoerceTemporal, + withoutIntegerChecks, + withoutStorageCollations; +import 'validation.dart' show sqlWords; + +/// A live database object outside the schema features modeled by the ORM. final class CatalogObject { + /// Catalog object category, such as `index`, `trigger`, or `view`. final String kind; + + /// Physical object name, as reported by the catalog reader. final String name; + + /// Catalog SQL or descriptive metadata preserved for manual review. final String definition; + + /// Records an object that the ORM cannot safely recreate from modeled fields. const CatalogObject(this.kind, this.name, this.definition); } +/// Physical table facts read from the selected database's catalog. +/// +/// [unmanaged] preserves descriptions of objects a migration must not silently +/// replace, such as custom triggers or unsupported index definitions. final class TableInfo { + /// Physical table name used for this inspection. final String name; + + /// Column storage metadata in catalog order. final List columns; + + /// Primary-key column names in key order; empty when no key exists. final List primaryKey; + + /// Modeled unique constraints, each retaining its own column order. final List> uniqueKeys; + + /// Modeled foreign-key columns, target tables, and deletion actions. final List foreignKeys; + + /// Index definitions expressible by the ORM's physical schema model. final List indexes; + + /// Objects requiring separate review before a table rebuild or schema change. final List unmanaged; + + /// Enforced row CHECK constraints recognized by the catalog reader. final List checks; + + /// Groups catalog facts without querying or changing the database. + /// + /// Supplied lists are retained; use [inspectTable] to obtain live metadata. const TableInfo({ required this.name, required this.columns, @@ -28,17 +83,24 @@ final class TableInfo { }); } +/// Differences between a declared snapshot and the live database catalog. final class SchemaVerification { + /// Human-readable mismatches in modeled schema facts. final List differences; + + /// Live objects that require separate review, even when [matches] is true. final List unmanaged; + + /// Records modeled differences and separately reviewable catalog objects. const SchemaVerification(this.differences, this.unmanaged); /// Equality of the declared schema facts; inspect unmanaged objects separately. bool get matches => differences.isEmpty; } +/// Reads one physical [table] without changing its schema or data. Future inspectTable(SqlDatabase db, String table) async { - if (_isMysql(db.dialect)) return _mysqlTable(db, table); + if (isMysqlFamily(db.dialect)) return mysqlTable(db, table); final columns = await inspectColumns(db, table); final primary = [], unique = >[], @@ -47,18 +109,18 @@ Future inspectTable(SqlDatabase db, String table) async { final checks = []; if (db.dialect == SqlDialect.sqlite) { final info = await db.execute( - SqlCommand('PRAGMA table_xinfo(${_quote(table)})'), + SqlCommand('PRAGMA table_xinfo(${quoteIdentifier(table)})'), ); final primaryRows = info.rows.where((r) => (r[5] as int) > 0).toList() ..sort((a, b) => (a[5] as int).compareTo(b[5] as int)); primary.addAll(primaryRows.map((r) => r[1] as String)); final list = await db.execute( - SqlCommand('PRAGMA index_list(${_quote(table)})'), + SqlCommand('PRAGMA index_list(${quoteIdentifier(table)})'), ); for (final row in list.rows) { final name = row[1] as String; final parts = await db.execute( - SqlCommand('PRAGMA index_xinfo(${_quote(name)})'), + SqlCommand('PRAGMA index_xinfo(${quoteIdentifier(name)})'), ); final keys = parts.rows.where((r) => r[5] == 1).toList(); final sql = await db.execute( @@ -89,7 +151,7 @@ Future inspectTable(SqlDatabase db, String table) async { } } final keys = await db.execute( - SqlCommand('PRAGMA foreign_key_list(${_quote(table)})'), + SqlCommand('PRAGMA foreign_key_list(${quoteIdentifier(table)})'), ); final grouped = >>{}; for (final row in keys.rows) { @@ -139,8 +201,8 @@ Future inspectTable(SqlDatabase db, String table) async { var withoutChecks = sql ?? ''; if (sql != null) { // Storage checks retain their existing column-width/precision meaning. - final remaining = _withoutIntegerChecks(sql, columns); - final parsed = _sqliteChecks(remaining); + final remaining = withoutIntegerChecks(sql, columns); + final parsed = sqliteChecks(remaining); checks.addAll(parsed.map((c) => CheckInfo(c.name, c.expression))); final text = StringBuffer(); var start = 0; @@ -151,8 +213,8 @@ Future inspectTable(SqlDatabase db, String table) async { withoutChecks = (text..write(remaining.substring(start))).toString(); } if (sql != null && - _sqlWords( - _withoutStorageCollations(_withoutComputed(withoutChecks), columns), + sqlWords( + withoutStorageCollations(withoutComputed(withoutChecks), columns), ).any( { 'CHECK', @@ -318,11 +380,15 @@ AND (c.relrowsecurity OR c.relforcerowsecurity ); } +/// Compares modeled tables, columns, keys, indexes, and checks with the catalog. +/// +/// This does not infer or apply a migration. Inspect the returned unmanaged +/// objects as well as the modeled differences before changing an existing table. Future verifySchema( SqlDatabase db, SchemaSnapshot expected, ) async { - if (_isMysql(db.dialect)) return _mysqlVerifySchema(db, expected); + if (isMysqlFamily(db.dialect)) return mysqlVerifySchema(db, expected); final differences = [], unmanaged = []; for (final table in expected.tables) { final actual = await inspectTable(db, table.name); @@ -337,7 +403,7 @@ Future verifySchema( differences.add('$path is missing'); continue; } - if (found.storageType != _columnStorageType(column, db.dialect)) { + if (found.storageType != columnStorageType(column, db.dialect)) { checkContextMatches = false; differences.add('$path type differs'); } @@ -347,11 +413,10 @@ Future verifySchema( if ((found.temporalPrecision ?? 6) != (column.temporalPrecision ?? 6)) { differences.add('$path temporal precision differs'); } - if (!_matchesDecimalDigits(column, found)) { + if (!matchesDecimalDigits(column, found)) { differences.add('$path decimal precision/scale differs'); } - if (db.dialect == SqlDialect.sqlite && - !_matchesCollation(column, found)) { + if (db.dialect == SqlDialect.sqlite && !matchesCollation(column, found)) { differences.add('$path collation differs'); } if (column.codec.sqlType == 'integer' && @@ -362,7 +427,7 @@ Future verifySchema( _columnDefault( column.defaultSql == null ? null - : _coerceColumn(column.defaultSql!, column, db.dialect), + : coerceColumn(column.defaultSql!, column, db.dialect), column, )) { differences.add('$path default differs'); @@ -383,7 +448,7 @@ Future verifySchema( differences.add('${table.name}.$name is unmanaged'); } differences.addAll( - await _verifyComputed( + await verifyComputed( db, table, actual.columns, @@ -391,19 +456,20 @@ Future verifySchema( ), ); void compare(String kind, Object? desired, Object? found) { - if (_hash(desired) != _hash(found)) { + if (migrationHash(desired) != migrationHash(found)) { differences.add('${table.name} $kind differs'); } } List set(Iterable values) => - values.map((v) => jsonEncode(_canonical(v))).toList()..sort(); + values.map((v) => jsonEncode(canonicalMigrationValue(v))).toList() + ..sort(); compare('primary key', table.primaryKey, actual.primaryKey); compare('unique keys', set(table.uniqueKeys), set(actual.uniqueKeys)); // Expected SQL may no longer resolve when a referenced column/type drifted. // Report that schema drift instead of attempting an invalid EXPLAIN. final checkMatches = checkContextMatches - ? await _matchChecks(db, table.name, table.checks, actual.checks) + ? await matchChecks(db, table.name, table.checks, actual.checks) : List.filled(table.checks.length, null); if (checkMatches.any((i) => i == null) || checkMatches.length != actual.checks.length) { @@ -419,13 +485,13 @@ Future verifySchema( } compare( 'foreign keys', - set(table.foreignKeys.map(_foreignKeyJson)), - set(actual.foreignKeys.map(_foreignKeyJson)), + set(table.foreignKeys.map(foreignKeyJson)), + set(actual.foreignKeys.map(foreignKeyJson)), ); compare( 'indexes', - set(table.indexes.map(_indexJson)), - set(actual.indexes.map(_indexJson)), + set(table.indexes.map(indexJson)), + set(actual.indexes.map(indexJson)), ); } return SchemaVerification( @@ -435,21 +501,21 @@ Future verifySchema( } String? _columnDefault(String? value, Column column) { - var normalized = _normalizeDefault(value); + var normalized = normalizeDefault(value); var temporalPrefix = ''; if (normalized != null && column.temporalPrecision != null && column.temporalPrecision != 6) { - final inner = _uncoerceTemporal( + final inner = uncoerceTemporal( normalized, column.codec.sqlType, column.temporalPrecision!, ); if (inner != null) { - normalized = _normalizeDefault(inner); + normalized = normalizeDefault(inner); temporalPrefix = 'temporal cast:'; } - final type = _columnStorageType( + final type = columnStorageType( column, SqlDialect.postgres, ).toLowerCase().replaceFirst('timestamptz', 'timestamp'); @@ -457,7 +523,7 @@ String? _columnDefault(String? value, Column column) { ? '$type with time zone' : type; if (normalized != null && normalized.endsWith('::$native')) { - normalized = _normalizeDefault( + normalized = normalizeDefault( normalized.substring(0, normalized.length - native.length - 2), ); } @@ -489,13 +555,13 @@ String? _columnDefault(String? value, Column column) { } final unwrapped = column.decimalPrecision == null ? null - : _uncoerceDecimalDefault( + : uncoerceDecimalDefault( normalized, column.decimalPrecision!, column.decimalScale ?? 0, ); final prefix = unwrapped == null ? '' : 'decimal cast:'; - normalized = unwrapped == null ? normalized : _normalizeDefault(unwrapped)!; + normalized = unwrapped == null ? normalized : normalizeDefault(unwrapped)!; // PostgreSQL removes the quotes from a NUMERIC literal. Compare finite // literals by value, leaving arbitrary SQL expressions unchanged. final literal = normalized.startsWith("'") && normalized.endsWith("'") @@ -504,7 +570,7 @@ String? _columnDefault(String? value, Column column) { return '$prefix${Decimal.tryParse(literal)?.toString() ?? normalized}'; } -String? _normalizeDefault(String? value) { +String? normalizeDefault(String? value) { if (value == null) return null; var normalized = value.trim(); while (normalized.startsWith('(') && normalized.endsWith(')')) { diff --git a/lib/src/migrate/checks.dart b/lib/src/migrate/checks.dart index 82538c86..169ab77b 100644 --- a/lib/src/migrate/checks.dart +++ b/lib/src/migrate/checks.dart @@ -1,13 +1,26 @@ -part of '../../migrate.dart'; +import 'dart:convert' show jsonEncode; + +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' show CheckSchema, TableSchema; +import '../../values.dart' show Codecs, OrmException; +import 'catalog.dart' show inspectTable; +import 'sql_utils.dart' show quoteIdentifier; +import 'sqlite_checks.dart' show sqliteName, sqliteTokens; /// An enforced, validated row CHECK read from this database's catalog. final class CheckInfo { + /// Database constraint name, or null for an unnamed constraint. final String? name; + + /// Catalog SQL expression evaluated by the row CHECK constraint. final String expression; + + /// Records one catalog CHECK without evaluating its expression. const CheckInfo(this.name, this.expression); } -TableSchema _withChecks(TableSchema table, List checks) => +TableSchema withChecks(TableSchema table, List checks) => TableSchema( table.name, columns: table.columns, @@ -18,7 +31,7 @@ TableSchema _withChecks(TableSchema table, List checks) => checks: checks, ); -({List removed, List added}) _checkDelta( +({List removed, List added}) checkDelta( List before, List after, SqlDialect dialect, @@ -40,7 +53,7 @@ TableSchema _withChecks(TableSchema table, List checks) => } String _checkSignature(String expression) { - var tokens = _sqliteTokens(expression); + var tokens = sqliteTokens(expression); // Strip enclosing parentheses, not parentheses that determine precedence. while (tokens.length >= 2 && tokens.first.text == '(' && @@ -61,10 +74,10 @@ String _checkSignature(String expression) { tokens .map( (t) => t.text.startsWith('i:') - ? _sqliteName(t.text.substring(2)) + ? sqliteName(t.text.substring(2)) : t.text.startsWith('s:') ? t.text - : _sqliteName(t.text), + : sqliteName(t.text), ) .toList(), ); @@ -74,7 +87,7 @@ String _checkSignature(String expression) { // planner to render both projections in the same typed context; never compare // expressions by deleting casts or precedence-bearing syntax. EXPLAIN does not // run the SELECT, though PostgreSQL can evaluate immutable constants in planning. -Future> _checkExpressions( +Future> checkExpressions( SqlDatabase db, String table, List expressions, @@ -86,7 +99,7 @@ Future> _checkExpressions( final result = await db.execute( SqlCommand( 'EXPLAIN (VERBOSE, FORMAT JSON, COSTS OFF) SELECT ' - '${expressions.map((e) => '($e\n)').join(', ')} FROM ONLY ${_quote(table)}', + '${expressions.map((e) => '($e\n)').join(', ')} FROM ONLY ${quoteIdentifier(table)}', ), ); final json = Codecs.json.decode(result.rows.single.single) as List; @@ -100,14 +113,14 @@ Future> _checkExpressions( } /// One actual constraint is consumed per declaration, including unnamed copies. -Future> _matchChecks( +Future> matchChecks( SqlDatabase db, String table, List expected, List actual, ) async { if (expected.isEmpty) return const []; - final signatures = await _checkExpressions(db, table, [ + final signatures = await checkExpressions(db, table, [ ...expected.map((c) => c.expression(db.dialect)), ...actual.map((c) => c.expression), ]); @@ -120,7 +133,7 @@ Future> _matchChecks( name == null || (db.dialect == SqlDialect.sqlite ? actual[j].name != null && - _sqliteName(name) == _sqliteName(actual[j].name!) + sqliteName(name) == sqliteName(actual[j].name!) : name == actual[j].name); if (!used.contains(j) && sameName && @@ -135,13 +148,13 @@ Future> _matchChecks( return matches; } -Future _dropCheck( +Future dropCheck( SqlDatabase db, String table, CheckSchema check, ) async { final actual = (await inspectTable(db, table)).checks; - final match = (await _matchChecks(db, table, [check], actual)).single; + final match = (await matchChecks(db, table, [check], actual)).single; if (match == null || actual[match].name == null) { throw OrmException( 'MIGRATION.DRIFT', @@ -150,7 +163,7 @@ Future _dropCheck( } await db.execute( SqlCommand( - 'ALTER TABLE ${_quote(table)} DROP CONSTRAINT ${_quote(actual[match].name!)}', + 'ALTER TABLE ${quoteIdentifier(table)} DROP CONSTRAINT ${quoteIdentifier(actual[match].name!)}', ), ); } diff --git a/lib/src/migrate/columns.dart b/lib/src/migrate/columns.dart new file mode 100644 index 00000000..9f5fa2c9 --- /dev/null +++ b/lib/src/migrate/columns.dart @@ -0,0 +1,314 @@ +// Live column inspection and comparison with declared storage. + +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' + show Column, ComputedColumn, ComputedStorage, TableSchema; +import 'catalog.dart' show normalizeDefault; +import 'computed.dart' + show computedDeclaration, sqliteComputedColumns, verifyComputed; +import 'mysql_catalog.dart' show mysqlColumns; +import 'mysql_schema.dart' show isMysqlFamily; +import 'schema.dart' show columnStorageType; +import 'sql_utils.dart' show quoteIdentifier; +import 'sqlite_checks.dart' + show + sqliteChecks, + sqliteColumnCollations, + sqliteDecimalDigits, + sqliteIntegerBits, + sqliteName, + sqliteTemporalPrecision, + uncoerceDecimalDefault, + uncoerceTemporal; + +/// Actual catalog columns, rather than a claimed migration version. +final class ColumnInfo { + /// Physical column name. + final String name; + + /// Database storage type as normalized by the engine's catalog reader. + final String storageType; + + /// Whether the inspected column permits SQL NULL. + final bool nullable; + + /// Catalog default expression, or null when no SQL default is present. + final String? defaultSql; + + /// Engine-specific generated, hidden, or identity flag from the catalog. + /// + /// [computed] independently describes a recognized computed expression. + final bool generated; + + /// Database-computed expression and storage mode, when recognized. + final ComputedColumn? computed; + + /// Recognized signed integer width, or null when not applicable or unknown. + final int? integerBits; + + /// Catalog collation name, including ORM storage collations where applicable. + final String? collation; + + /// Recognized maximum decimal digits, or null for unconstrained/other storage. + final int? decimalPrecision; + + /// Recognized decimal fractional digits, or null when not constrained. + final int? decimalScale; + + /// Recognized fractional-second precision, or null for other storage. + final int? temporalPrecision; + + /// Records physical column facts without interpreting them as a Dart model. + const ColumnInfo({ + required this.name, + required this.storageType, + required this.nullable, + this.defaultSql, + this.generated = false, + this.computed, + this.integerBits, + this.collation, + this.decimalPrecision, + this.decimalScale, + this.temporalPrecision, + }); + + /// Removes the managed SQLite default coercion when drafting a declaration. + String? get declarationDefaultSql => + defaultSql != null && storageType == 'TEXT' && decimalPrecision != null + ? uncoerceDecimalDefault( + normalizeDefault(defaultSql)!, + decimalPrecision!, + decimalScale ?? 0, + ) ?? + defaultSql + : defaultSql != null && storageType == 'TEXT' && temporalPrecision != null + ? uncoerceTemporal( + normalizeDefault(defaultSql)!, + temporalCollationKind(collation)!, + temporalPrecision!, + ) ?? + defaultSql + : defaultSql; + + /// SQL suitable for a declaration, without ORM storage coercion wrappers. + ComputedColumn? get declarationComputed => computedDeclaration(this); +} + +/// Reads storage types, nullability, defaults, and computed-column metadata. +Future> inspectColumns( + SqlDatabase db, + String table, +) async { + if (isMysqlFamily(db.dialect)) return mysqlColumns(db, table); + if (db.dialect == SqlDialect.sqlite) { + final rows = await db.execute( + SqlCommand('PRAGMA table_xinfo(${quoteIdentifier(table)})'), + ); + final indexes = await db.execute( + SqlCommand('PRAGMA index_list(${quoteIdentifier(table)})'), + ); + final rowidPrimaryKey = !indexes.rows.any((r) => r[3] == 'pk'); + final ddl = await db.execute( + SqlCommand( + "SELECT sql FROM main.sqlite_schema WHERE type = 'table' AND name = ?1", + [table], + ), + ); + final sql = ddl.rows.firstOrNull?.first as String? ?? ''; + final checks = sqliteChecks(sql); + final computed = sqliteComputedColumns(sql); + final collations = { + for (final c in sqliteColumnCollations(sql)) + sqliteName(c.column): c.collation, + }; + final columns = []; + for (final row in rows.rows) { + final name = row[1] as String; + final type = (row[2] as String).toUpperCase(); + final collation = collations[sqliteName(name)] ?? 'BINARY'; + final digits = + type == 'TEXT' && collation.toLowerCase() == 'orm_decimal_v1' + ? sqliteDecimalDigits(name, checks) + : null; + columns.add( + ColumnInfo( + name: name, + storageType: type, + collation: collation, + decimalPrecision: digits?.$1, + decimalScale: digits?.$2, + temporalPrecision: + type == 'TEXT' && temporalCollationKind(collation) != null + ? sqliteTemporalPrecision( + name, + temporalCollationKind(collation)!, + checks, + ) + : null, + nullable: row[3] == 0 && !(row[5] != 0 && rowidPrimaryKey), + defaultSql: row[4] as String?, + generated: (row[6] as int) > 0, + computed: computed[sqliteName(name)], + integerBits: type == 'INTEGER' + ? sqliteIntegerBits(name, checks) + : null, + ), + ); + } + return columns; + } + + final result = await db.execute( + SqlCommand( + ''' +SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), + NOT a.attnotnull, pg_get_expr(d.adbin, d.adrelid), + a.attidentity <> '' OR a.attgenerated <> '', a.attgenerated::text +FROM pg_catalog.pg_attribute a +JOIN pg_catalog.pg_class c ON c.oid = a.attrelid +JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace +LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum +WHERE c.relname = \$1 AND n.nspname = current_schema() AND a.attnum > 0 AND NOT a.attisdropped +ORDER BY a.attnum''', + [table], + ), + ); + return [ + for (final row in result.rows) + ColumnInfo( + name: row[0] as String, + storageType: _postgresStorageName(row[1] as String), + temporalPrecision: _postgresTemporalPrecision(row[1] as String), + nullable: row[2] as bool, + defaultSql: row[5] == '' ? row[3] as String? : null, + generated: row[4] as bool, + computed: row[5] == '' + ? null + : ComputedColumn( + row[3] as String, + storage: row[5] == 's' + ? ComputedStorage.stored + : ComputedStorage.virtual, + ), + decimalPrecision: _postgresDecimalDigits(row[1] as String)?.$1, + decimalScale: _postgresDecimalDigits(row[1] as String)?.$2, + integerBits: switch ((row[1] as String).toUpperCase()) { + 'SMALLINT' => 16, + 'INTEGER' => 32, + 'BIGINT' => 64, + _ => null, + }, + ), + ]; +} + +/// Column drift check. Constraints, indexes and unmanaged objects are separate +/// catalog checks; this method does not pretend that columns prove full equality. +Future> verifyColumns( + SqlDatabase db, + List tables, +) async { + final differences = []; + for (final table in tables) { + final inspected = await inspectColumns(db, table.name); + final actual = {for (final c in inspected) c.name: c}; + var contextMatches = true; + for (final expected in table.columns) { + final column = actual.remove(expected.name); + final path = '${table.name}.${expected.name}'; + if (column == null) { + contextMatches = false; + differences.add('$path is missing'); + continue; + } + if (column.storageType != columnStorageType(expected, db.dialect)) { + contextMatches = false; + differences.add('$path type is ${column.storageType}'); + } + if (column.nullable != expected.nullable) { + differences.add('$path nullability differs'); + } + if ((column.temporalPrecision ?? 6) != + (expected.temporalPrecision ?? 6)) { + differences.add('$path temporal precision differs'); + } + if (!matchesDecimalDigits(expected, column)) { + differences.add('$path decimal precision/scale differs'); + } + if (db.dialect == SqlDialect.sqlite && + !matchesCollation(expected, column)) { + differences.add('$path collation differs'); + } + if (expected.codec.sqlType == 'integer' && + (column.integerBits ?? 64) != (expected.integerBits ?? 64)) { + differences.add('$path integer width differs'); + } + } + for (final extra in actual.keys) { + differences.add('${table.name}.$extra is unmanaged'); + } + differences.addAll( + await verifyComputed( + db, + table, + inspected, + contextMatches: contextMatches, + ), + ); + } + return differences; +} + +bool matchesCollation(Column expected, ColumnInfo actual) => + (actual.collation ?? 'BINARY').toLowerCase() == + sqliteCollation(expected.codec.sqlType); + +String sqliteCollation(String type) => switch (type) { + 'decimal' || + 'date' || + 'time' || + 'local_datetime' || + 'instant' => 'orm_${type}_v1', + _ => 'binary', +}; + +bool matchesDecimalDigits(Column expected, ColumnInfo actual) => + expected.decimalPrecision == actual.decimalPrecision && + (expected.decimalScale ?? 0) == (actual.decimalScale ?? 0); + +(int, int)? _postgresDecimalDigits(String type) { + final match = RegExp( + r'^numeric\((\d+),(-?\d+)\)$', + caseSensitive: false, + ).firstMatch(type.replaceAll(' ', '')); + return match == null ? null : (int.parse(match[1]!), int.parse(match[2]!)); +} + +String? temporalCollationKind(String? collation) => + switch (collation?.toLowerCase()) { + 'orm_time_v1' => 'time', + 'orm_local_datetime_v1' => 'local_datetime', + 'orm_instant_v1' => 'instant', + _ => null, + }; + +int? _postgresTemporalPrecision(String type) { + final match = RegExp( + r'^(?:time|timestamp)\(([0-6])\) (?:with|without) time zone$', + caseSensitive: false, + ).firstMatch(type); + return match == null ? null : int.parse(match[1]!); +} + +String _postgresStorageName(String type) { + var name = type.toUpperCase().replaceFirstMapped( + RegExp(r'^TIMESTAMP(\([0-6]\))? WITH TIME ZONE$'), + (m) => 'TIMESTAMPTZ${m[1] ?? ''}', + ); + if (_postgresTemporalPrecision(type) == 6) { + name = name.replaceFirst('(6)', ''); + } + return name; +} diff --git a/lib/src/migrate/computed.dart b/lib/src/migrate/computed.dart index 94c3e5b2..6609fbfc 100644 --- a/lib/src/migrate/computed.dart +++ b/lib/src/migrate/computed.dart @@ -1,6 +1,14 @@ -part of '../../migrate.dart'; +import '../../driver.dart' show Backend, SqlDialect; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' + show Column, ComputedColumn, ComputedStorage, TableSchema; +import 'checks.dart' show checkExpressions; +import 'columns.dart' show ColumnInfo, temporalCollationKind; +import 'schema.dart' show coerceColumn, columnStorageType; +import 'sqlite_checks.dart' + show sqliteName, sqliteTokens, uncoerceDecimalDefault, uncoerceTemporal; -Map _computedJson(ComputedColumn value) => { +Map computedJson(ComputedColumn value) => { 'sqlite': value.sqlite, 'postgres': value.postgres, if (value.mysql != null && value.mysql != value.postgres) @@ -10,15 +18,15 @@ Map _computedJson(ComputedColumn value) => { 'storage': value.storage.name, }; -typedef _SqliteComputed = ({ +typedef SqliteComputed = ({ String column, ComputedColumn value, int start, int end, }); -List<_SqliteComputed> _sqliteComputed(String sql) { - final tokens = _sqliteTokens(sql), result = <_SqliteComputed>[]; +List _sqliteComputed(String sql) { + final tokens = sqliteTokens(sql), result = []; var depth = 0, beginning = false; String? column; for (var i = 0; i < tokens.length; i++) { @@ -84,11 +92,11 @@ List<_SqliteComputed> _sqliteComputed(String sql) { return result; } -Map _sqliteComputedColumns(String sql) => { - for (final c in _sqliteComputed(sql)) _sqliteName(c.column): c.value, +Map sqliteComputedColumns(String sql) => { + for (final c in _sqliteComputed(sql)) sqliteName(c.column): c.value, }; -String _withoutComputed(String sql) { +String withoutComputed(String sql) { final result = StringBuffer(); var start = 0; for (final c in _sqliteComputed(sql)) { @@ -98,14 +106,14 @@ String _withoutComputed(String sql) { return (result..write(sql.substring(start))).toString(); } -ComputedColumn? _declarationComputed(ColumnInfo column) { +ComputedColumn? computedDeclaration(ColumnInfo column) { final value = column.computed; if (value != null && column.storageType == 'TEXT' && column.temporalPrecision != null) { - final expression = _uncoerceTemporal( + final expression = uncoerceTemporal( value.sqlite, - _temporalCollationKind(column.collation)!, + temporalCollationKind(column.collation)!, column.temporalPrecision!, ); return expression == null @@ -117,7 +125,7 @@ ComputedColumn? _declarationComputed(ColumnInfo column) { column.decimalPrecision == null) { return value; } - final expression = _uncoerceDecimalDefault( + final expression = uncoerceDecimalDefault( value.sqlite, column.decimalPrecision!, column.decimalScale ?? 0, @@ -127,7 +135,7 @@ ComputedColumn? _declarationComputed(ColumnInfo column) { : ComputedColumn(expression, storage: value.storage); } -Future> _verifyComputed( +Future> verifyComputed( SqlDatabase db, TableSchema table, List actual, { @@ -148,7 +156,7 @@ Future> _verifyComputed( } paths.add(path); final pair = [ - _coerceColumn(expected.expression(db.dialect), column, db.dialect), + coerceColumn(expected.expression(db.dialect), column, db.dialect), observed.expression(db.dialect), ]; // PostgreSQL stores assignment casts in pg_attrdef. Compare both sides in @@ -157,12 +165,12 @@ Future> _verifyComputed( db.dialect == SqlDialect.postgres ? pair.map( (sql) => - 'CAST(($sql\n) AS ${_columnStorageType(column, db.dialect)})', + 'CAST(($sql\n) AS ${columnStorageType(column, db.dialect)})', ) : pair, ); } - final signatures = await _checkExpressions(db, table.name, expressions); + final signatures = await checkExpressions(db, table.name, expressions); for (var i = 0; i < paths.length; i++) { if (signatures[i * 2] != signatures[i * 2 + 1]) { differences.add('${paths[i]} computed expression or storage differs'); @@ -171,7 +179,7 @@ Future> _verifyComputed( return differences; } -TableSchema _materializedColumns(TableSchema table) => TableSchema( +TableSchema materializedColumns(TableSchema table) => TableSchema( table.name, columns: [ for (final c in table.columns) diff --git a/lib/src/migrate/diff.dart b/lib/src/migrate/diff.dart index 37c55178..f5703552 100644 --- a/lib/src/migrate/diff.dart +++ b/lib/src/migrate/diff.dart @@ -1,14 +1,45 @@ -part of '../../migrate.dart'; +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' + show Column, ComputedStorage, ForeignKey, IndexSchema, TableSchema; +import '../../values.dart' show OrmException; +import 'checks.dart' show checkDelta, withChecks; +import 'computed.dart' show materializedColumns; +import 'migration.dart' show Migration; +import 'mysql_diff.dart' show mysqlDiff; +import 'mysql_schema.dart' show isMysqlFamily; +import 'schema.dart' + show + checkDefinition, + coerceColumn, + columnDefinition, + columnStorageType, + createIndexSql, + createTable, + foreignKey, + sameStorage; +import 'snapshot.dart' + show SchemaSnapshot, columnJson, foreignKeyJson, indexJson; +import 'sql_utils.dart' show migrationHash, quoteIdentifier; +import 'step.dart' + show DropConstraint, DropTable, ExecuteSql, MigrationStep, RebuildTable; /// Explicit physical renames. Column maps are keyed by the final table name. /// Swaps and chains need separate migrations to make intermediate names explicit. final class SchemaRenames { + /// Existing physical table names mapped to their intended new names. final Map tables; + + /// Old-to-new column names, grouped by the table's final physical name. final Map> columns; + + /// Declares intentional renames without inferring them from schema similarity. + /// + /// Maps are retained as provided. Diff generation validates targets and rejects + /// swaps or chains that need explicit intermediate migration steps. const SchemaRenames({this.tables = const {}, this.columns = const {}}); } -Migration _diff( +Migration diffSchema( String id, { required SqlDialect dialect, required SchemaSnapshot from, @@ -18,8 +49,8 @@ Migration _diff( required bool allowDestructive, required Map> using, }) { - if (_isMysql(dialect)) { - return _mysqlDiff( + if (isMysqlFamily(dialect)) { + return mysqlDiff( id, dialect: dialect, from: from, @@ -56,7 +87,7 @@ Migration _diff( for (final entry in renames.tables.entries) { renameSql.add( ExecuteSql( - 'ALTER TABLE ${_quote(entry.key)} RENAME TO ${_quote(entry.value)}', + 'ALTER TABLE ${quoteIdentifier(entry.key)} RENAME TO ${quoteIdentifier(entry.value)}', ), ); } @@ -80,7 +111,7 @@ Migration _diff( for (final rename in entry.value.entries) { renameSql.add( ExecuteSql( - 'ALTER TABLE ${_quote(entry.key)} RENAME COLUMN ${_quote(rename.key)} TO ${_quote(rename.value)}', + 'ALTER TABLE ${quoteIdentifier(entry.key)} RENAME COLUMN ${quoteIdentifier(rename.key)} TO ${quoteIdentifier(rename.value)}', ), ); } @@ -212,7 +243,7 @@ Migration _diff( '$name.${c.name} needs a reviewed PostgreSQL replacement/materialization migration for this computed-mode change.', ); } - if (prior != null && c.computed == null && !_sameStorage(prior, c)) { + if (prior != null && c.computed == null && !sameStorage(prior, c)) { if (using[name]?[c.name] == null) { throw OrmException( 'MIGRATION.CAST', @@ -233,7 +264,7 @@ Migration _diff( if (old == null || next == null || next.computed != null || - _sameStorage(old, next) || + sameStorage(old, next) || entry.value.trim().isEmpty) { throw OrmException( 'MIGRATION.CAST', @@ -248,15 +279,17 @@ Migration _diff( if (dialect == SqlDialect.sqlite) ...computedRenames, }) { if (dialect == SqlDialect.sqlite) { - var target = _withChecks(table, const []); + var target = withChecks(table, const []); if (computedRenames.contains(table)) { - target = _materializedColumns(target); + target = materializedColumns(target); } steps.add( RebuildTable( table, target, - copy: {for (final c in table.columns) c.name: _quote(c.name)}, + copy: { + for (final c in table.columns) c.name: quoteIdentifier(c.name), + }, ), ); } else { @@ -278,15 +311,15 @@ Migration _diff( final a = before[name], b = after[name]; return a == null || b == null || - _hash([ + migrationHash([ a.primaryKey, a.uniqueKeys, - a.indexes.where((i) => i.unique).map(_indexJson).toList(), + a.indexes.where((i) => i.unique).map(indexJson).toList(), ]) != - _hash([ + migrationHash([ b.primaryKey, b.uniqueKeys, - b.indexes.where((i) => i.unique).map(_indexJson).toList(), + b.indexes.where((i) => i.unique).map(indexJson).toList(), ]); } @@ -295,8 +328,7 @@ Migration _diff( return a == null || b == null || a.columns.any( - (c) => - b.columns.any((n) => c.name == n.name && !_sameStorage(c, n)), + (c) => b.columns.any((n) => c.name == n.name && !sameStorage(c, n)), ); } @@ -310,7 +342,7 @@ Migration _diff( typesChanged(key.target) || typesChanged(old.name)) { steps.add( - DropConstraint(old.name, {'kind': 'f', ..._foreignKeyJson(key)}), + DropConstraint(old.name, {'kind': 'f', ...foreignKeyJson(key)}), ); if (retained) addedForeignKeys.add((old.name, key)); } @@ -329,9 +361,9 @@ Migration _diff( } for (final name in addedTables) { final table = after[name]!; - steps.add(ExecuteSql(_createTable(table, dialect))); + steps.add(ExecuteSql(createTable(table, dialect))); for (final index in table.indexes) { - steps.add(ExecuteSql(_createIndex(name, index))); + steps.add(ExecuteSql(createIndexSql(name, index))); } if (dialect == SqlDialect.postgres) { addedForeignKeys.addAll(table.foreignKeys.map((k) => (name, k))); @@ -341,7 +373,7 @@ Migration _diff( var old = before[name]!; if (dialect == SqlDialect.sqlite && computedRenames.any((t) => tableName(t.name) == name)) { - old = _materializedColumns(old); + old = materializedColumns(old); } final next = after[name]!; final oldColumns = {for (final c in old.columns) c.name: c}; @@ -353,22 +385,22 @@ Migration _diff( .intersection(newColumns.keys.toSet()) .where( (c) => - _hash(_columnJson(oldColumns[c]!)) != - _hash(_columnJson(newColumns[c]!)), + migrationHash(columnJson(oldColumns[c]!)) != + migrationHash(columnJson(newColumns[c]!)), ) .toList(); final keysChanged = - _hash([ + migrationHash([ old.primaryKey, old.uniqueKeys, - old.foreignKeys.map(_foreignKeyJson).toList(), + old.foreignKeys.map(foreignKeyJson).toList(), ]) != - _hash([ + migrationHash([ next.primaryKey, next.uniqueKeys, - next.foreignKeys.map(_foreignKeyJson).toList(), + next.foreignKeys.map(foreignKeyJson).toList(), ]); - final checks = _checkDelta(old.checks, next.checks, dialect); + final checks = checkDelta(old.checks, next.checks, dialect); if (dialect == SqlDialect.sqlite && (removed.isNotEmpty || changed.isNotEmpty || @@ -385,7 +417,7 @@ Migration _diff( (c) => oldColumns.containsKey(c) && newColumns[c]!.computed == null, )) - column: using[name]?[column] ?? _quote(column), + column: using[name]?[column] ?? quoteIdentifier(column), }, ), ); @@ -401,42 +433,46 @@ Migration _diff( }), ); } - if (_hash(old.primaryKey) != _hash(next.primaryKey) && + if (migrationHash(old.primaryKey) != migrationHash(next.primaryKey) && old.primaryKey.isNotEmpty) { steps.add( DropConstraint(name, {'kind': 'p', 'columns': old.primaryKey}), ); } for (final key in old.uniqueKeys) { - if (!next.uniqueKeys.any((k) => _hash(k) == _hash(key))) { + if (!next.uniqueKeys.any( + (k) => migrationHash(k) == migrationHash(key), + )) { steps.add(DropConstraint(name, {'kind': 'u', 'columns': key})); } } } for (final index in old.indexes) { if (!next.indexes.any( - (i) => _hash(_indexJson(i)) == _hash(_indexJson(index)), + (i) => migrationHash(indexJson(i)) == migrationHash(indexJson(index)), )) { - steps.add(ExecuteSql('DROP INDEX ${_quote(index.name)}')); + steps.add(ExecuteSql('DROP INDEX ${quoteIdentifier(index.name)}')); } } for (final column in removed) { steps.add( - ExecuteSql('ALTER TABLE ${_quote(name)} DROP COLUMN ${_quote(column)}'), + ExecuteSql( + 'ALTER TABLE ${quoteIdentifier(name)} DROP COLUMN ${quoteIdentifier(column)}', + ), ); } for (final column in added) { steps.add( ExecuteSql( - 'ALTER TABLE ${_quote(name)} ADD COLUMN ${_columnDefinition(newColumns[column]!, dialect)}', + 'ALTER TABLE ${quoteIdentifier(name)} ADD COLUMN ${columnDefinition(newColumns[column]!, dialect)}', ), ); } for (final column in changed) { final a = oldColumns[column]!, b = newColumns[column]!; final prefix = - 'ALTER TABLE ${_quote(name)} ALTER COLUMN ${_quote(column)}'; - final typeChanged = !_sameStorage(a, b); + 'ALTER TABLE ${quoteIdentifier(name)} ALTER COLUMN ${quoteIdentifier(column)}'; + final typeChanged = !sameStorage(a, b); if (a.computed != null && b.computed == null) { steps.add(ExecuteSql('$prefix DROP EXPRESSION')); } @@ -446,7 +482,7 @@ Migration _diff( if (typeChanged) { steps.add( ExecuteSql( - '$prefix TYPE ${_columnStorageType(b, dialect)}' + '$prefix TYPE ${columnStorageType(b, dialect)}' '${b.computed != null ? '' : ' USING (${using[name]![column]})'}', ), ); @@ -456,7 +492,7 @@ Migration _diff( typeChanged)) { steps.add( ExecuteSql( - '$prefix SET EXPRESSION AS (${_coerceColumn(b.computed!.expression(dialect), b, dialect)}\n)', + '$prefix SET EXPRESSION AS (${coerceColumn(b.computed!.expression(dialect), b, dialect)}\n)', ), ); } @@ -478,24 +514,26 @@ Migration _diff( if (checks.added.isNotEmpty) { steps.add( ExecuteSql( - 'ALTER TABLE ${_quote(name)} ' - '${checks.added.map((c) => 'ADD ${_checkDefinition(c, dialect)}').join(', ')}', + 'ALTER TABLE ${quoteIdentifier(name)} ' + '${checks.added.map((c) => 'ADD ${checkDefinition(c, dialect)}').join(', ')}', ), ); } - if (_hash(old.primaryKey) != _hash(next.primaryKey) && + if (migrationHash(old.primaryKey) != migrationHash(next.primaryKey) && next.primaryKey.isNotEmpty) { steps.add( ExecuteSql( - 'ALTER TABLE ${_quote(name)} ADD PRIMARY KEY (${next.primaryKey.map(_quote).join(', ')})', + 'ALTER TABLE ${quoteIdentifier(name)} ADD PRIMARY KEY (${next.primaryKey.map(quoteIdentifier).join(', ')})', ), ); } for (final key in next.uniqueKeys) { - if (!old.uniqueKeys.any((k) => _hash(k) == _hash(key))) { + if (!old.uniqueKeys.any( + (k) => migrationHash(k) == migrationHash(key), + )) { steps.add( ExecuteSql( - 'ALTER TABLE ${_quote(name)} ADD UNIQUE (${key.map(_quote).join(', ')})', + 'ALTER TABLE ${quoteIdentifier(name)} ADD UNIQUE (${key.map(quoteIdentifier).join(', ')})', ), ); } @@ -503,15 +541,15 @@ Migration _diff( } for (final index in next.indexes) { if (!old.indexes.any( - (i) => _hash(_indexJson(i)) == _hash(_indexJson(index)), + (i) => migrationHash(indexJson(i)) == migrationHash(indexJson(index)), )) { - steps.add(ExecuteSql(_createIndex(name, index))); + steps.add(ExecuteSql(createIndexSql(name, index))); } } } for (final (name, key) in addedForeignKeys) { steps.add( - ExecuteSql('ALTER TABLE ${_quote(name)} ADD ${_foreignKey(key)}'), + ExecuteSql('ALTER TABLE ${quoteIdentifier(name)} ADD ${foreignKey(key)}'), ); } return Migration.steps( @@ -524,7 +562,7 @@ Migration _diff( } bool _sameForeignKey(ForeignKey a, ForeignKey b) => - _hash(_foreignKeyJson(a)) == _hash(_foreignKeyJson(b)); + migrationHash(foreignKeyJson(a)) == migrationHash(foreignKeyJson(b)); bool _canAddSqlite(Column column) => column.computed != null ? column.computed!.storage == ComputedStorage.virtual : column.defaultSql == null || diff --git a/lib/src/migrate/execute.dart b/lib/src/migrate/execute.dart new file mode 100644 index 00000000..3932e889 --- /dev/null +++ b/lib/src/migrate/execute.dart @@ -0,0 +1,209 @@ +// Transactional execution of reviewed SQL and SQLite table rebuilds. + +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' show CheckSchema; +import '../../values.dart' show OrmException; +import 'catalog.dart' show inspectTable, verifySchema; +import 'checks.dart' show dropCheck, matchChecks; +import 'schema.dart' show coerceColumn, createIndexSql, createTable; +import 'snapshot.dart' show SchemaSnapshot, indexJson; +import 'sql_utils.dart' show migrationHash, quoteIdentifier; +import 'step.dart' + show + Backfill, + CheckedSql, + CheckedTableSql, + DropConstraint, + DropTable, + ExecuteSql, + MigrationStep, + RebuildTable; + +Future executeStep(SqlDatabase db, MigrationStep step) async { + switch (step) { + case CheckedSql() || CheckedTableSql() || Backfill(): + throw const OrmException( + 'MIGRATION.TRANSACTION', + 'This step requires the recovery runner.', + ); + case ExecuteSql(): + await db.execute(SqlCommand(step.sql)); + case DropTable(): + await db.execute(SqlCommand('DROP TABLE ${quoteIdentifier(step.table)}')); + case RebuildTable(): + await _rebuild(db, step); + case DropConstraint(): + if (step.constraint['kind'] == 'c') { + await dropCheck( + db, + step.table, + CheckSchema( + step.constraint['name'] as String?, + step.constraint['expression'] as String, + ), + ); + return; + } + final rows = await db.execute( + SqlCommand( + r''' +SELECT c.conname, c.contype::text, + ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY k(num, ord) + JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.num ORDER BY k.ord), + t.relname, + ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY k(num, ord) + JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.num ORDER BY k.ord), + c.confdeltype::text +FROM pg_constraint c JOIN pg_class r ON r.oid = c.conrelid +JOIN pg_namespace n ON n.oid = r.relnamespace +LEFT JOIN pg_class t ON t.oid = c.confrelid +WHERE n.nspname = current_schema() AND r.relname = $1''', + [step.table], + ), + ); + final matches = rows.rows + .where( + (row) => + migrationHash({ + 'kind': row[1], + 'columns': row[2], + if (row[1] == 'f') ...{ + 'target': row[3], + 'targetColumns': row[4], + 'onDelete': switch (row[5]) { + 'a' => 'NO ACTION', + 'r' => 'RESTRICT', + 'c' => 'CASCADE', + 'n' => 'SET NULL', + 'd' => 'SET DEFAULT', + _ => '', + }, + }, + }) == + migrationHash(step.constraint), + ) + .toList(); + if (matches.length != 1) { + throw OrmException( + 'MIGRATION.DRIFT', + 'Expected exactly one matching constraint on ${step.table}; found ${matches.length}.', + ); + } + await db.execute( + SqlCommand( + 'ALTER TABLE ${quoteIdentifier(step.table)} DROP CONSTRAINT ${quoteIdentifier(matches.single.first as String)}', + ), + ); + } +} + +Future _rebuild(SqlDatabase db, RebuildTable step) async { + final before = step.before, after = step.after, name = after.name; + final actual = await inspectTable(db, name); + final checkMatches = await matchChecks( + db, + name, + before.checks, + actual.checks, + ); + if (actual.checks.length > checkMatches.whereType().length) { + throw OrmException( + 'MIGRATION.UNMANAGED', + '$name has undeclared CHECK constraints.', + ); + } + final unsupported = actual.unmanaged + .where((o) => !{'index', 'trigger', 'view'}.contains(o.kind)) + .toList(); + if (unsupported.isNotEmpty || + actual.columns.any((c) => c.generated && c.computed == null)) { + throw OrmException( + 'MIGRATION.UNMANAGED', + '$name has unmodeled table constraints/options; provide a manual migration to preserve them.', + ); + } + final check = await verifySchema(db, SchemaSnapshot([before])); + final differences = check.differences + .where((d) => d != '$name indexes differs') + .toList(); + for (final index in before.indexes) { + if (!actual.indexes.any( + (i) => migrationHash(indexJson(i)) == migrationHash(indexJson(index)), + )) { + differences.add('$name.${index.name} index differs'); + } + } + if (differences.isNotEmpty) { + throw OrmException('MIGRATION.DRIFT', differences.join('\n')); + } + final removed = before.columns + .map((c) => c.name) + .toSet() + .difference(after.columns.map((c) => c.name).toSet()); + if (removed.isNotEmpty && actual.unmanaged.any((o) => o.kind == 'trigger')) { + throw OrmException( + 'MIGRATION.UNMANAGED', + '$name has triggers and removed columns; update them in a manual migration.', + ); + } + final objects = await db.execute( + SqlCommand( + "SELECT type, name, sql FROM sqlite_schema WHERE tbl_name = ?1 AND type IN ('index', 'trigger') AND sql IS NOT NULL ORDER BY type, name", + [name], + ), + ); + final managedIndexes = before.indexes.map((i) => i.name).toSet(); + final targetIndexes = after.indexes.map((i) => i.name).toSet(); + for (final row in objects.rows) { + if (row[0] == 'index' && + !managedIndexes.contains(row[1]) && + targetIndexes.contains(row[1])) { + throw OrmException( + 'MIGRATION.UNMANAGED', + 'New index ${row[1]} conflicts with an unmanaged index.', + ); + } + } + final temporary = '_orm_rebuild_$name'; + await db.execute( + SqlCommand(createTable(after, SqlDialect.sqlite, name: temporary)), + ); + await db.execute( + SqlCommand( + 'INSERT INTO ${quoteIdentifier(temporary)} (${step.copy.keys.map(quoteIdentifier).join(', ')}) ' + 'SELECT ${step.copy.entries.map((e) => coerceColumn(e.value, after.columns.firstWhere((c) => c.name == e.key), db.dialect)).join(', ')} FROM ${quoteIdentifier(name)}', + ), + ); + await db.execute(SqlCommand('DROP TABLE ${quoteIdentifier(name)}')); + // Views remain present while their source is temporarily absent. Legacy mode + // prevents SQLite from reparsing those views during this one rename. + final legacy = (await db.execute(SqlCommand('PRAGMA legacy_alter_table'))) + .rows + .single + .single; + try { + await db.execute(SqlCommand('PRAGMA legacy_alter_table = ON')); + await db.execute( + SqlCommand( + 'ALTER TABLE ${quoteIdentifier(temporary)} RENAME TO ${quoteIdentifier(name)}', + ), + ); + } finally { + await db.execute( + SqlCommand('PRAGMA legacy_alter_table = ${legacy == 1 ? 'ON' : 'OFF'}'), + ); + } + for (final index in after.indexes) { + await db.execute(SqlCommand(createIndexSql(name, index))); + } + for (final row in objects.rows) { + if (row[0] == 'index' && managedIndexes.contains(row[1])) continue; + await db.execute(SqlCommand(row[2] as String)); + } + for (final view in actual.unmanaged.where((o) => o.kind == 'view')) { + await db.execute( + SqlCommand('SELECT * FROM ${quoteIdentifier(view.name)} LIMIT 0'), + ); + } +} diff --git a/lib/src/migrate/history.dart b/lib/src/migrate/history.dart index 93af2982..cf5c1fb3 100644 --- a/lib/src/migrate/history.dart +++ b/lib/src/migrate/history.dart @@ -1,121 +1,60 @@ -part of '../../migrate.dart'; - -/// One reviewed plan for one database engine. A history cannot change engines. -final class Migration { - final String id; - final SqlDialect dialect; - final List steps; - final SchemaSnapshot? snapshot; - final String? previous; - Migration( - String id, - List statements, { - required SqlDialect dialect, - SchemaSnapshot? snapshot, - String? previous, - }) : this.steps( - id, - statements.map(ExecuteSql.new).toList(), - dialect: dialect, - snapshot: snapshot, - previous: previous, - ); - - Migration.steps( - this.id, - List steps, { - required this.dialect, - SchemaSnapshot? snapshot, - this.previous, - }) : steps = List.unmodifiable(steps.map((s) => _targetStep(s, dialect))), - snapshot = snapshot?.forDialect(dialect) { - if (!RegExp(r'^[0-9]+_[a-z][a-z0-9_]*$').hasMatch(id)) { - throw ArgumentError( - 'Migration IDs use a numeric prefix and lowercase name.', - ); - } - } - - factory Migration.create( - String id, - List schema, { - required SqlDialect dialect, - }) => Migration.steps( - id, - _isMysql(dialect) - ? _mysqlCreateSteps(schema, dialect) - : [ - for (final command in createSchema(schema, dialect)) - ExecuteSql(command.sql), - ], - dialect: dialect, - snapshot: SchemaSnapshot(schema), - ); - - factory Migration.diff( - String id, { - required SqlDialect dialect, - required SchemaSnapshot from, - required SchemaSnapshot to, - SchemaRenames renames = const SchemaRenames(), - String? previous, - bool allowDestructive = false, - Map> using = const {}, - }) => _diff( - id, - dialect: dialect, - from: from.forDialect(dialect), - to: to.forDialect(dialect), - renames: renames, - previous: previous, - allowDestructive: allowDestructive, - using: using, - ); - - Map toJson() => { - 'format': 3, - 'id': id, - 'dialect': dialect.name, - if (snapshot != null) 'snapshot': snapshot!.toJson(), - if (previous != null) 'previous': previous, - 'steps': [for (final step in steps) step.toJson()], - }; - late final String checksum = _hash(toJson()); -} - -MigrationStep _targetStep(MigrationStep step, SqlDialect dialect) => - switch (step) { - CheckedTableSql() => CheckedTableSql( - step.sql, - before: step.before == null - ? null - : _targetTable(step.before!, dialect), - after: step.after == null ? null : _targetTable(step.after!, dialect), - ), - RebuildTable() => RebuildTable( - _targetTable(step.before, dialect), - _targetTable(step.after, dialect), - copy: step.copy, - ), - Backfill() => Backfill( - _targetTable(step.table, dialect), - set: step.set, - where: step.where, - doneWhen: step.doneWhen, - batchSize: step.batchSize, - ), - _ => step, - }; +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' + show + MariadbTransaction, + MysqlTransaction, + PostgresTransaction, + SqlDatabase, + SqliteTransaction; +import '../../values.dart' show OrmException; +import 'backfill.dart' show BackfillBudget, parameterMarker; +import 'catalog.dart' show SchemaVerification, verifySchema; +import 'execute.dart' show executeStep; +import 'migration.dart' show Migration; +import 'mysql_recovery.dart' show applyRecoverableMysql, mysqlBaseline; +import 'mysql_schema.dart' show isMysqlFamily; +import 'progress.dart' show MigrationProgress; +import 'recovery.dart' + show + applyRecoverable, + applyRecoverableSqlite, + checkMigrationVersion, + hasMigrationTable, + historyDdl, + loadMigrationProgress, + migrationSession, + migrationTransaction, + needsRebuild, + validateProgress; +import 'snapshot.dart' show SchemaSnapshot; +import 'step.dart' show Backfill, CheckedSql; +import 'validation.dart' show validateMigrations; +/// The identifier and recorded fingerprint of one fully applied migration. final class MigrationStatus { + /// Identifier of a fully applied migration. final String id; + + /// Fingerprint recorded when that migration was applied or baselined. final String checksum; + + /// Records a completed history entry without validating a bundled definition. const MigrationStatus(this.id, this.checksum); } +/// Validates and executes a fixed history against one raw SQL runtime. +/// +/// The caller owns [database] and closes it after migration work finishes. +/// Applying or baselining acquires the engine's migration lock. A migrator never +/// guesses a rename or silently accepts a changed applied migration. final class Migrator { + /// Runtime used for migration catalog reads and writes. final SqlDatabase database; + + /// Maximum wait for the database's migration lock. final Duration lockTimeout; + + /// Uses an existing runtime; constructing a migrator performs no database I/O. const Migrator( this.database, { this.lockTimeout = const Duration(seconds: 30), @@ -126,8 +65,8 @@ final class Migrator { List migrations, { required SchemaSnapshot expected, }) async { - if (_isMysql(database.dialect)) { - return _mysqlBaseline(this, migrations, expected); + if (isMysqlFamily(database.dialect)) { + return mysqlBaseline(this, migrations, expected); } validateMigrations(migrations, dialect: database.dialect); if (database.inTransaction) { @@ -144,13 +83,13 @@ final class Migrator { 'The final migration must carry the expected baseline snapshot.', ); } - return _migrationSession( + return migrationSession( database, lockTimeout, (session) => session.transaction( (tx) async { if ((await Migrator(tx).history()).isNotEmpty || - (await _progress(tx)).isNotEmpty) { + (await loadMigrationProgress(tx)).isNotEmpty) { throw const OrmException( 'MIGRATION.BASELINE', 'Migration history already exists.', @@ -169,7 +108,7 @@ final class Migrator { ), ); for (final migration in migrations) { - await _recordMigration(tx, migration); + await recordMigration(tx, migration); } return verification; }, @@ -180,8 +119,11 @@ final class Migrator { ); } + /// Returns completed migrations in identifier order. + /// + /// An absent history table produces an empty list without creating it. Future> history() async { - if (!await _hasMigrationTable(database, '_orm_migrations')) return []; + if (!await hasMigrationTable(database, '_orm_migrations')) return []; final rows = await database.execute( SqlCommand('SELECT id, checksum FROM "_orm_migrations" ORDER BY id'), ); @@ -192,14 +134,17 @@ final class Migrator { } /// Durable step checkpoints, including failed stages of partially applied migrations. - Future> progress() => _progress(database); + Future> progress() => loadMigrationProgress(database); + /// Validates the supplied history and returns its unapplied suffix. + /// + /// Reads recorded checksums and recovery checkpoints without applying DDL. Future> plan(List migrations) async { validateMigrations(migrations, dialect: database.dialect); - if (!database.inSession) await _checkMigrationVersion(database); + if (!database.inSession) await checkMigrationVersion(database); final applied = await history(); _validateApplied(migrations, applied); - _validateProgress(migrations, applied, await progress(), database.dialect); + validateProgress(migrations, applied, await progress(), database.dialect); return migrations.skip(applied.length).toList(growable: false); } @@ -256,7 +201,7 @@ final class Migrator { ); } } - _validateProgress(migrations, applied, checkpoints, tx.dialect); + validateProgress(migrations, applied, checkpoints, tx.dialect); final current = applied.length - 1; if (current < lower || current > upper) { throw OrmException( @@ -285,8 +230,10 @@ final class Migrator { } /// Applies pending migrations, returning the IDs completed by this call. - /// Ordinary migration batches are atomic. [CheckedSql] and [Backfill] opt - /// their containing migration into durable per-step recovery. + /// SQLite/PostgreSQL batches of transactional steps are atomic. [CheckedSql] + /// and [Backfill] opt their containing migration into durable per-step + /// recovery. MySQL/MariaDB DDL uses checked, recoverable steps because the + /// database can commit schema changes implicitly. /// /// [maxBackfillBatches] bounds nonempty data batches across this invocation. /// Reaching the limit returns normally with unfinished migrations still @@ -307,42 +254,42 @@ final class Migrator { } validateMigrations(migrations, dialect: database.dialect); Future> run(SqlDatabase session) async { - if (_isMysql(session.dialect)) { - return _applyRecoverableMysql( + if (isMysqlFamily(session.dialect)) { + return applyRecoverableMysql( session, migrations, - _BackfillBudget(maxBackfillBatches), + BackfillBudget(maxBackfillBatches), ); } if (migrations.any( (m) => m.steps.any((s) => s is CheckedSql || s is Backfill), )) { return session.dialect == SqlDialect.sqlite - ? _applyRecoverableSqlite( + ? applyRecoverableSqlite( session, migrations, - _BackfillBudget(maxBackfillBatches), + BackfillBudget(maxBackfillBatches), ) - : _applyRecoverable( + : applyRecoverable( session, migrations, - _BackfillBudget(maxBackfillBatches), + BackfillBudget(maxBackfillBatches), ); } - return _migrationTransaction(session, (tx) async { - await tx.execute(SqlCommand(_historyDdl)); + return migrationTransaction(session, (tx) async { + await tx.execute(SqlCommand(historyDdl)); final pending = await Migrator(tx).plan(migrations); for (final migration in pending) { for (final step in migration.steps) { - await _executeStep(tx, step); + await executeStep(tx, step); } - await _recordMigration(tx, migration); + await recordMigration(tx, migration); } return [for (final migration in pending) migration.id]; - }, rebuild: _needsRebuild(migrations, session.dialect)); + }, rebuild: needsRebuild(migrations, session.dialect)); } - return _migrationSession(database, lockTimeout, run); + return migrationSession(database, lockTimeout, run); } } @@ -367,176 +314,10 @@ void _validateApplied( } } -/// Validates local ordering, checksum links and operations without opening a database. -void validateMigrations( - List migrations, { - required SqlDialect dialect, -}) { - String? last; - String? previousChecksum; - for (final migration in migrations) { - if (last != null && last.compareTo(migration.id) >= 0) { - throw const OrmException( - 'MIGRATION.ORDER', - 'Migration IDs must be unique and strictly ordered.', - ); - } - last = migration.id; - if (migration.previous != null && migration.previous != previousChecksum) { - throw const OrmException( - 'MIGRATION.CHAIN', - 'Migration previous-checksum chain is broken.', - ); - } - previousChecksum = migration.checksum; - if (migration.dialect != dialect) { - throw OrmException( - 'MIGRATION.TARGET', - '${migration.id} targets ${migration.dialect.name}, but this history requires ${dialect.name}.', - ); - } - if (_isMysql(dialect) && migration.id.length > 191) { - throw const OrmException( - 'MIGRATION.ID', - 'MySQL migration IDs must not exceed 191 characters.', - ); - } - for (final step in migration.steps) { - if (step is CheckedTableSql) { - if (!_isMysql(dialect)) { - throw const OrmException( - 'MIGRATION.TARGET', - 'CheckedTableSql requires MySQL or MariaDB.', - ); - } - for (final table in [ - step.before, - step.after, - ].whereType()) { - _validateSchema([table], dialect); - } - _validateSql(step.sql, transactional: false); - _validateMysqlStatement(step.sql); - if (!{ - 'CREATE', - 'ALTER', - 'DROP', - 'RENAME', - }.contains(_sqlWords(step.sql).first)) { - throw const OrmException( - 'MIGRATION.DDL', - 'CheckedTableSql must contain a reviewed table DDL statement.', - ); - } - continue; - } - if (step is Backfill) { - _validateSchema([step.table], dialect); - continue; - } - if (step is CheckedSql) { - if (dialect != SqlDialect.postgres) { - throw const OrmException( - 'MIGRATION.TARGET', - 'Recoverable autocommit migrations currently require PostgreSQL.', - ); - } - _validateSql(step.sql, transactional: false); - for (final probe in [step.readyWhen, step.doneWhen]) { - if (_sqlWords(probe).firstOrNull != 'SELECT') { - throw const OrmException( - 'MIGRATION.PROBE', - 'Recovery conditions must be SELECT queries.', - ); - } - } - continue; - } - if (step is DropTable) { - if (_isMysql(dialect)) { - throw const OrmException( - 'MIGRATION.RECOVERY', - 'MySQL table removal requires CheckedTableSql with its before schema.', - ); - } - continue; - } - if (step is RebuildTable) { - if (dialect != SqlDialect.sqlite) { - throw const OrmException( - 'MIGRATION.TARGET', - 'Table rebuilds are SQLite operations.', - ); - } - _validateSchema([step.before], dialect); - _validateSchema([step.after], dialect); - continue; - } - if (step is DropConstraint) { - if (dialect != SqlDialect.postgres) { - throw const OrmException( - 'MIGRATION.TARGET', - 'Constraint resolution is a PostgreSQL operation.', - ); - } - continue; - } - final sql = (step as ExecuteSql).sql; - _validateSql(sql, transactional: true); - if (_isMysql(dialect)) { - _validateMysqlStatement(sql); - } - if (_isMysql(dialect) && - !{ - 'INSERT', - 'UPDATE', - 'DELETE', - 'REPLACE', - }.contains(_sqlWords(sql).first)) { - throw const OrmException( - 'MIGRATION.RECOVERY', - 'MySQL ExecuteSql accepts DML only. DDL requires CheckedTableSql with reviewed before/after schemas.', - ); - } - } - } -} - -void _validateSql(String sql, {required bool transactional}) { - final words = _sqlWords(sql); - if (words.isEmpty) { - throw const OrmException( - 'MIGRATION.EMPTY', - 'Migration contains an empty statement.', - ); - } - if ({ - 'BEGIN', - 'COMMIT', - 'ROLLBACK', - 'END', - 'SAVEPOINT', - 'RELEASE', - 'PRAGMA', - 'START', - 'ABORT', - }.contains(words.first) || - (transactional && - (words.first == 'VACUUM' || words.contains('CONCURRENTLY')))) { - throw const OrmException( - 'MIGRATION.TRANSACTION', - 'Transaction control belongs to the runner; autocommit operations need CheckedSql.', - ); - } -} - -Future _recordMigration( - SqlDatabase db, - Migration migration, -) => db +Future recordMigration(SqlDatabase db, Migration migration) => db .execute( SqlCommand( - 'INSERT INTO "_orm_migrations" (id, checksum, applied_at) VALUES (${[for (var i = 1; i <= 3; i++) _mark(db.dialect, i)].join(', ')})', + 'INSERT INTO "_orm_migrations" (id, checksum, applied_at) VALUES (${[for (var i = 1; i <= 3; i++) parameterMarker(db.dialect, i)].join(', ')})', [ migration.id, migration.checksum, @@ -547,57 +328,3 @@ Future _recordMigration( .then((_) {}); // Only classifies transaction control. The database remains the SQL parser. -List _sqlWords(String sql) { - final words = []; - var i = 0; - while (i < sql.length) { - if (sql.startsWith('--', i)) { - final end = sql.indexOf('\n', i + 2); - i = end < 0 ? sql.length : end + 1; - } else if (sql.startsWith('/*', i)) { - var depth = 1; - i += 2; - while (i < sql.length && depth > 0) { - if (sql.startsWith('/*', i)) { - depth++; - i += 2; - } else if (sql.startsWith('*/', i)) { - depth--; - i += 2; - } else { - i++; - } - } - } else if (sql[i] == "'" || sql[i] == '"') { - final quote = sql[i++]; - while (i < sql.length) { - if (sql[i++] == quote) { - if (i < sql.length && sql[i] == quote) { - i++; - } else { - break; - } - } - } - } else if (sql[i] == r'$') { - final tag = RegExp(r'^\$[a-zA-Z0-9_]*\$') - .firstMatch(sql.substring(i)) - ?.group(0); - if (tag == null) { - i++; - } else { - final end = sql.indexOf(tag, i + tag.length); - i = end < 0 ? sql.length : end + tag.length; - } - } else if (RegExp(r'[a-zA-Z_]').hasMatch(sql[i])) { - final start = i++; - while (i < sql.length && RegExp(r'[a-zA-Z0-9_]').hasMatch(sql[i])) { - i++; - } - words.add(sql.substring(start, i).toUpperCase()); - } else { - i++; - } - } - return words; -} diff --git a/lib/src/migrate/migration.dart b/lib/src/migrate/migration.dart new file mode 100644 index 00000000..341144b7 --- /dev/null +++ b/lib/src/migrate/migration.dart @@ -0,0 +1,141 @@ +// Immutable, engine-specific migration plans and schema differences. + +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' show TableSchema; +import 'diff.dart' show SchemaRenames, diffSchema; +import 'mysql_schema.dart' show isMysqlFamily, mysqlCreateSteps; +import 'schema.dart' show createSchema; +import 'snapshot.dart' show SchemaSnapshot, targetTable; +import 'sql_utils.dart' show migrationHash; +import 'step.dart' + show Backfill, CheckedTableSql, ExecuteSql, MigrationStep, RebuildTable; + +/// One reviewed plan for one database engine. A history cannot change engines. +final class Migration { + /// Ordered identifier, such as `0001_create_users`. + final String id; + + /// The only database engine on which this plan can run. + final SqlDialect dialect; + + /// Immutable operations, executed in their reviewed order. + final List steps; + + /// Physical schema expected after all steps complete, when recorded. + final SchemaSnapshot? snapshot; + + /// Checksum of the immediately preceding migration, when recorded. + final String? previous; + + /// Builds a plan from transactional SQL statements. + /// + /// Use [Migration.steps] for rebuilds, checked DDL, or bounded backfills. + Migration( + String id, + List statements, { + required SqlDialect dialect, + SchemaSnapshot? snapshot, + String? previous, + }) : this.steps( + id, + statements.map(ExecuteSql.new).toList(), + dialect: dialect, + snapshot: snapshot, + previous: previous, + ); + + /// Builds a plan with explicit operations and freezes its engine-specific schema. + Migration.steps( + this.id, + List steps, { + required this.dialect, + SchemaSnapshot? snapshot, + this.previous, + }) : steps = List.unmodifiable(steps.map((s) => _targetStep(s, dialect))), + snapshot = snapshot?.forDialect(dialect) { + if (!RegExp(r'^[0-9]+_[a-z][a-z0-9_]*$').hasMatch(id)) { + throw ArgumentError( + 'Migration IDs use a numeric prefix and lowercase name.', + ); + } + } + + /// Plans creation of a new schema for [dialect], without database access. + factory Migration.create( + String id, + List schema, { + required SqlDialect dialect, + }) => Migration.steps( + id, + isMysqlFamily(dialect) + ? mysqlCreateSteps(schema, dialect) + : [ + for (final command in createSchema(schema, dialect)) + ExecuteSql(command.sql), + ], + dialect: dialect, + snapshot: SchemaSnapshot(schema), + ); + + /// Plans the transition from [from] to [to], without applying any changes. + /// + /// Renames must be explicit in [renames]. Potential data loss requires + /// [allowDestructive]; [using] supplies reviewed conversion SQL by table and + /// column. Review the returned steps before adding the plan to a history. + factory Migration.diff( + String id, { + required SqlDialect dialect, + required SchemaSnapshot from, + required SchemaSnapshot to, + SchemaRenames renames = const SchemaRenames(), + String? previous, + bool allowDestructive = false, + Map> using = const {}, + }) => diffSchema( + id, + dialect: dialect, + from: from.forDialect(dialect), + to: to.forDialect(dialect), + renames: renames, + previous: previous, + allowDestructive: allowDestructive, + using: using, + ); + + /// Canonical operation data used for reporting and checksum calculation. + /// + /// Saved migration files remain Dart source; this is not a persistence format. + Map toJson() => { + 'format': 3, + 'id': id, + 'dialect': dialect.name, + if (snapshot != null) 'snapshot': snapshot!.toJson(), + if (previous != null) 'previous': previous, + 'steps': [for (final step in steps) step.toJson()], + }; + + /// Stable fingerprint of SQL, operations, schema, engine, and prior checksum. + late final String checksum = migrationHash(toJson()); +} + +MigrationStep _targetStep(MigrationStep step, SqlDialect dialect) => + switch (step) { + CheckedTableSql() => CheckedTableSql( + step.sql, + before: step.before == null ? null : targetTable(step.before!, dialect), + after: step.after == null ? null : targetTable(step.after!, dialect), + ), + RebuildTable() => RebuildTable( + targetTable(step.before, dialect), + targetTable(step.after, dialect), + copy: step.copy, + ), + Backfill() => Backfill( + targetTable(step.table, dialect), + set: step.set, + where: step.where, + doneWhen: step.doneWhen, + batchSize: step.batchSize, + ), + _ => step, + }; diff --git a/lib/src/migrate/mysql_catalog.dart b/lib/src/migrate/mysql_catalog.dart index fa45b867..e3ee03ae 100644 --- a/lib/src/migrate/mysql_catalog.dart +++ b/lib/src/migrate/mysql_catalog.dart @@ -1,4 +1,20 @@ -part of '../../migrate.dart'; +import 'dart:convert' show jsonEncode, utf8; + +import '../../driver.dart' show Backend, SqlCommand, SqlDialect, SqlResult; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' + show Column, ComputedColumn, ForeignKey, IndexSchema; +import '../../values.dart' show Codecs, Decimal; +import 'catalog.dart' + show CatalogObject, SchemaVerification, TableInfo, normalizeDefault; +import 'checks.dart' show CheckInfo; +import 'columns.dart' show ColumnInfo; +import 'mysql_schema.dart' + show mysqlColumnType, mysqlForeignName, mysqlPhysicalTable, mysqlUniqueName; +import 'snapshot.dart' show SchemaSnapshot, foreignKeyJson, indexJson; +import 'sql_utils.dart' + show canonicalMigrationValue, migrationHash, quoteIdentifier; +import 'sqlite_checks.dart' show sqliteTokens; // information_schema reports some textual metadata with a BLOB wire type. // Decode only these known catalog-query result cells, never application BLOBs. @@ -9,9 +25,9 @@ List> _mysqlCatalogRows(SqlResult result) => [ String _mysqlExpression(String? expression) { if (expression == null) return ''; - final normalized = _normalizeDefault(expression)!; + final normalized = normalizeDefault(expression)!; if (normalized.toUpperCase() == 'NULL') return ''; - final tokens = _sqliteTokens(normalized); + final tokens = sqliteTokens(normalized); final normalizedTokens = [ for (var i = 0; i < tokens.length; i++) if (!(tokens[i].text == '_UTF8MB4' && @@ -123,7 +139,7 @@ Object? _mysqlExpressionTree(List input) { String _mysqlDefaultExpression(String? expression, Column column) { if (expression == null) return ''; - final sql = _normalizeDefault(expression)!; + final sql = normalizeDefault(expression)!; if (sql.toUpperCase() == 'NULL') return ''; final clock = RegExp( r'^(?:CURRENT_TIMESTAMP|NOW)(?:\(([0-6]?)\))?$', @@ -132,7 +148,7 @@ String _mysqlDefaultExpression(String? expression, Column column) { if (clock != null) { return 'current_timestamp:${clock[1]?.isNotEmpty == true ? clock[1] : '0'}'; } - final tokens = _sqliteTokens(sql); + final tokens = sqliteTokens(sql); if (tokens.length == 1 && {'TRUE', 'FALSE'}.contains(tokens.single.text) && { @@ -188,7 +204,7 @@ String _mysqlCatalogDefault(String expression, SqlDialect dialect) { // of NO_BACKSLASH_ESCAPES. Return valid SQL for our fixed session mode. final result = StringBuffer(); var offset = 0; - for (final token in _sqliteTokens(expression)) { + for (final token in sqliteTokens(expression)) { if (!token.text.startsWith('s:')) continue; final value = token.text .substring(2) @@ -237,7 +253,7 @@ FROM information_schema.CHECK_CONSTRAINTS WHERE CONSTRAINT_SCHEMA=DATABASE() AND ]; } -Future> _mysqlColumns( +Future> mysqlColumns( SqlDatabase db, String table, ) async { @@ -277,7 +293,7 @@ ColumnInfo _mysqlColumnInfo( (c) => c.name == name && _mysqlExpression(c.expression) == - _mysqlExpression('json_valid(${_quote(name)})'), + _mysqlExpression('json_valid(${quoteIdentifier(name)})'), ); final storage = declared.contains('unsigned') || declared.contains('zerofill') ? declared.toUpperCase() @@ -341,8 +357,8 @@ ColumnInfo _mysqlColumnInfo( ); } -Future _mysqlTable(SqlDatabase db, String table) async { - final columns = await _mysqlColumns(db, table); +Future mysqlTable(SqlDatabase db, String table) async { + final columns = await mysqlColumns(db, table); final primary = [], unique = >[], indexes = []; @@ -363,7 +379,7 @@ FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?''' unmanaged.add(CatalogObject('table options', table, row.toString())); } final shown = await db.execute( - SqlCommand('SHOW CREATE TABLE ${_quote(table)}'), + SqlCommand('SHOW CREATE TABLE ${quoteIdentifier(table)}'), ); if (shown.rows.isNotEmpty && RegExp( @@ -439,7 +455,7 @@ FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME= if (entry.key == 'PRIMARY') { primary.addAll(keys); } else if (rows.first[1] == 0 && - entry.key == _mysqlUniqueName(table, keys)) { + entry.key == mysqlUniqueName(table, keys)) { unique.add(keys); } else { indexes.add(IndexSchema(entry.key, keys, unique: rows.first[1] == 0)); @@ -473,7 +489,7 @@ ORDER BY k.CONSTRAINT_NAME, k.ORDINAL_POSITION''', onDelete: row[4] as String, ); foreign.add(key); - if (entry.key != _mysqlForeignName(table, key)) { + if (entry.key != mysqlForeignName(table, key)) { unmanaged.add( CatalogObject('constraint name', entry.key, rows.toString()), ); @@ -487,7 +503,9 @@ ORDER BY k.CONSTRAINT_NAME, k.ORDINAL_POSITION''', column.storageType == 'JSON' && check.name == column.name && _mysqlExpression(check.expression) == - _mysqlExpression('json_valid(${_quote(column.name)})'), + _mysqlExpression( + 'json_valid(${quoteIdentifier(column.name)})', + ), ), ) .toList(); @@ -519,14 +537,14 @@ FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA=DATABASE() AND EVENT_OBJEC ); } -Future _mysqlVerifySchema( +Future mysqlVerifySchema( SqlDatabase db, SchemaSnapshot expected, ) async { final differences = [], unmanaged = []; for (final source in expected.tables) { - final table = _mysqlPhysicalTable(source), - actual = await _mysqlTable(db, source.name); + final table = mysqlPhysicalTable(source), + actual = await mysqlTable(db, source.name); unmanaged.addAll(actual.unmanaged); final columns = {for (final column in actual.columns) column.name: column}; for (final column in table.columns) { @@ -536,7 +554,7 @@ Future _mysqlVerifySchema( differences.add('$name is missing'); continue; } - if (found.storageType != _mysqlColumnType(column)) { + if (found.storageType != mysqlColumnType(column)) { differences.add('$name type differs'); } if (found.nullable != column.nullable) { @@ -559,23 +577,24 @@ Future _mysqlVerifySchema( differences.add('${table.name}.$name is unmanaged'); } List set(Iterable items) => - items.map((item) => jsonEncode(_canonical(item))).toList()..sort(); + items.map((item) => jsonEncode(canonicalMigrationValue(item))).toList() + ..sort(); void compare(String name, Object expected, Object found) { - if (_hash(expected) != _hash(found)) { + if (migrationHash(expected) != migrationHash(found)) { differences.add('${table.name} $name differs'); } } Map fk(ForeignKey key) => { - ..._foreignKeyJson(key), + ...foreignKeyJson(key), 'onDelete': key.onDelete == 'NO ACTION' ? 'RESTRICT' : key.onDelete, }; compare('primary key', table.primaryKey, actual.primaryKey); compare('unique keys', set(table.uniqueKeys), set(actual.uniqueKeys)); compare( 'indexes', - set(table.indexes.map(_indexJson)), - set(actual.indexes.map(_indexJson)), + set(table.indexes.map(indexJson)), + set(actual.indexes.map(indexJson)), ); compare( 'foreign keys', diff --git a/lib/src/migrate/mysql_diff.dart b/lib/src/migrate/mysql_diff.dart index 568e3c54..8074ad4c 100644 --- a/lib/src/migrate/mysql_diff.dart +++ b/lib/src/migrate/mysql_diff.dart @@ -1,6 +1,25 @@ -part of '../../migrate.dart'; +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' show Column, IndexSchema, TableSchema; +import '../../values.dart' show OrmException; +import 'checks.dart' show checkDelta; +import 'diff.dart' show SchemaRenames; +import 'migration.dart' show Migration; +import 'mysql_schema.dart' + show + mysqlAddForeign, + mysqlColumn, + mysqlCopy, + mysqlCreateTable, + mysqlForeignName, + mysqlPhysicalTable, + mysqlUniqueName; +import 'schema.dart' show checkDefinition, sameStorage; +import 'snapshot.dart' + show SchemaSnapshot, columnJson, foreignKeyJson, indexJson; +import 'sql_utils.dart' show migrationHash, quoteIdentifier; +import 'step.dart' show CheckedTableSql, MigrationStep; -Migration _mysqlDiff( +Migration mysqlDiff( String id, { required SqlDialect dialect, required SchemaSnapshot from, @@ -17,10 +36,10 @@ Migration _mysqlDiff( ); } final state = { - for (final table in from.tables) table.name: _mysqlPhysicalTable(table), + for (final table in from.tables) table.name: mysqlPhysicalTable(table), }; final target = { - for (final table in to.tables) table.name: _mysqlPhysicalTable(table), + for (final table in to.tables) table.name: mysqlPhysicalTable(table), }; final steps = []; void validateNames( @@ -69,7 +88,7 @@ Migration _mysqlDiff( if (after != null) state[after.name] = after; } - bool same(Object? a, Object? b) => _hash(a) == _hash(b); + bool same(Object? a, Object? b) => migrationHash(a) == migrationHash(b); bool columnChanges(String table, List columns) { final old = state[table], next = target[tableName(table)]; if (old == null || next == null || tableName(table) != table) return true; @@ -81,7 +100,7 @@ Migration _mysqlDiff( return prior == null || after == null || name != after.name || - !_sameStorage(prior, after); + !sameStorage(prior, after); }); } @@ -97,7 +116,7 @@ Migration _mysqlDiff( columnChanges(key.target, key.targetColumns) || !next.foreignKeys.any( (candidate) => - same(_foreignKeyJson(key), _foreignKeyJson(candidate)), + same(foreignKeyJson(key), foreignKeyJson(candidate)), ) || !same( state[key.target]?.primaryKey, @@ -111,9 +130,9 @@ Migration _mysqlDiff( .toList(); if (drop.isNotEmpty) { append( - 'ALTER TABLE ${_quote(before.name)} ${drop.map((key) => 'DROP FOREIGN KEY ${_quote(_mysqlForeignName(before.name, key))}').join(', ')}', + 'ALTER TABLE ${quoteIdentifier(before.name)} ${drop.map((key) => 'DROP FOREIGN KEY ${quoteIdentifier(mysqlForeignName(before.name, key))}').join(', ')}', before, - _mysqlCopy( + mysqlCopy( before, foreignKeys: before.foreignKeys .where((key) => !drop.contains(key)) @@ -130,32 +149,32 @@ Migration _mysqlDiff( 'Dropping ${before.name} requires allowDestructive.', ); } - append('DROP TABLE ${_quote(before.name)}', before, null); + append('DROP TABLE ${quoteIdentifier(before.name)}', before, null); } for (final entry in renames.tables.entries) { final before = state[entry.key]!; final changes = [ for (final key in before.uniqueKeys) - 'RENAME INDEX ${_quote(_mysqlUniqueName(entry.key, key))} TO ${_quote(_mysqlUniqueName(entry.value, key))}', - 'RENAME TO ${_quote(entry.value)}', + 'RENAME INDEX ${quoteIdentifier(mysqlUniqueName(entry.key, key))} TO ${quoteIdentifier(mysqlUniqueName(entry.value, key))}', + 'RENAME TO ${quoteIdentifier(entry.value)}', ]; append( - 'ALTER TABLE ${_quote(before.name)} ${changes.join(', ')}', + 'ALTER TABLE ${quoteIdentifier(before.name)} ${changes.join(', ')}', before, - _mysqlCopy(before, name: entry.value), + mysqlCopy(before, name: entry.value), ); } for (final after in target.values) { final before = state[after.name]; if (before == null) { - final created = _mysqlCopy(after, foreignKeys: []); - append(_mysqlCreateTable(created, dialect), null, created); + final created = mysqlCopy(after, foreignKeys: []); + append(mysqlCreateTable(created, dialect), null, created); continue; } final names = renames.columns[after.name] ?? const {}; String renamed(String name) => names[name] ?? name; final operations = []; - final desired = _mysqlCopy(after, foreignKeys: before.foreignKeys); + final desired = mysqlCopy(after, foreignKeys: before.foreignKeys); final removed = before.columns .where( (column) => !after.columns.any((c) => c.name == renamed(column.name)), @@ -172,7 +191,7 @@ Migration _mysqlDiff( .where((c) => c.name == renamed(column.name)) .firstOrNull; if (next == null) { - operations.add('DROP COLUMN ${_quote(column.name)}'); + operations.add('DROP COLUMN ${quoteIdentifier(column.name)}'); continue; } if (column.generated != next.generated) { @@ -181,7 +200,7 @@ Migration _mysqlDiff( 'Identity changes require a reviewed manual migration.', ); } - if (!_sameStorage(column, next) && !_mysqlSafeWiden(column, next)) { + if (!sameStorage(column, next) && !_mysqlSafeWiden(column, next)) { throw OrmException( 'MIGRATION.CAST', '${after.name}.${next.name} requires a reviewed replacement-column/backfill migration.', @@ -196,10 +215,10 @@ Migration _mysqlDiff( } if (column.name != next.name) { operations.add( - 'CHANGE COLUMN ${_quote(column.name)} ${_mysqlColumn(next, dialect)}', + 'CHANGE COLUMN ${quoteIdentifier(column.name)} ${mysqlColumn(next, dialect)}', ); - } else if (!same(_columnJson(column), _columnJson(next))) { - operations.add('MODIFY COLUMN ${_mysqlColumn(next, dialect)}'); + } else if (!same(columnJson(column), columnJson(next))) { + operations.add('MODIFY COLUMN ${mysqlColumn(next, dialect)}'); } } for (final column in after.columns) { @@ -213,13 +232,13 @@ Migration _mysqlDiff( '${after.name}.${column.name} needs a nullable/defaulted stage and explicit backfill.', ); } - operations.add('ADD COLUMN ${_mysqlColumn(column, dialect)}'); + operations.add('ADD COLUMN ${mysqlColumn(column, dialect)}'); } if (!same(before.primaryKey.map(renamed).toList(), after.primaryKey)) { if (before.primaryKey.isNotEmpty) operations.add('DROP PRIMARY KEY'); if (after.primaryKey.isNotEmpty) { operations.add( - 'ADD PRIMARY KEY (${after.primaryKey.map(_quote).join(', ')})', + 'ADD PRIMARY KEY (${after.primaryKey.map(quoteIdentifier).join(', ')})', ); } } @@ -227,14 +246,14 @@ Migration _mysqlDiff( for (final key in before.uniqueKeys) { final mapped = key.map(renamed).toList(); final i = pendingUnique.indexWhere((k) => same(k, mapped)); - final oldName = _mysqlUniqueName(after.name, key); + final oldName = mysqlUniqueName(after.name, key); if (i < 0) { - operations.add('DROP INDEX ${_quote(oldName)}'); + operations.add('DROP INDEX ${quoteIdentifier(oldName)}'); } else { - final nextName = _mysqlUniqueName(after.name, mapped); + final nextName = mysqlUniqueName(after.name, mapped); if (oldName != nextName) { operations.add( - 'RENAME INDEX ${_quote(oldName)} TO ${_quote(nextName)}', + 'RENAME INDEX ${quoteIdentifier(oldName)} TO ${quoteIdentifier(nextName)}', ); } pendingUnique.removeAt(i); @@ -242,7 +261,7 @@ Migration _mysqlDiff( } for (final key in pendingUnique) { operations.add( - 'ADD CONSTRAINT ${_quote(_mysqlUniqueName(after.name, key))} UNIQUE (${key.map(_quote).join(', ')})', + 'ADD CONSTRAINT ${quoteIdentifier(mysqlUniqueName(after.name, key))} UNIQUE (${key.map(quoteIdentifier).join(', ')})', ); } final pendingIndexes = after.indexes.toList(); @@ -253,20 +272,20 @@ Migration _mysqlDiff( unique: index.unique, ); final i = pendingIndexes.indexWhere( - (candidate) => same(_indexJson(mapped), _indexJson(candidate)), + (candidate) => same(indexJson(mapped), indexJson(candidate)), ); if (i < 0) { - operations.add('DROP INDEX ${_quote(index.name)}'); + operations.add('DROP INDEX ${quoteIdentifier(index.name)}'); } else { pendingIndexes.removeAt(i); } } for (final index in pendingIndexes) { operations.add( - 'ADD ${index.unique ? 'UNIQUE ' : ''}INDEX ${_quote(index.name)} (${index.columns.map(_quote).join(', ')})', + 'ADD ${index.unique ? 'UNIQUE ' : ''}INDEX ${quoteIdentifier(index.name)} (${index.columns.map(quoteIdentifier).join(', ')})', ); } - final checks = _checkDelta(before.checks, after.checks, dialect); + final checks = checkDelta(before.checks, after.checks, dialect); for (final check in checks.removed) { if (check.name == null) { throw const OrmException( @@ -275,15 +294,15 @@ Migration _mysqlDiff( ); } operations.add( - 'DROP ${dialect == SqlDialect.mysql ? 'CHECK' : 'CONSTRAINT'} ${_quote(check.name!)}', + 'DROP ${dialect == SqlDialect.mysql ? 'CHECK' : 'CONSTRAINT'} ${quoteIdentifier(check.name!)}', ); } for (final check in checks.added) { - operations.add('ADD ${_checkDefinition(check, dialect)}'); + operations.add('ADD ${checkDefinition(check, dialect)}'); } if (operations.isNotEmpty) { append( - 'ALTER TABLE ${_quote(after.name)} ${operations.join(', ')}', + 'ALTER TABLE ${quoteIdentifier(after.name)} ${operations.join(', ')}', before, desired, ); @@ -294,13 +313,13 @@ Migration _mysqlDiff( final add = after.foreignKeys .where( (key) => !before.foreignKeys.any( - (old) => same(_foreignKeyJson(old), _foreignKeyJson(key)), + (old) => same(foreignKeyJson(old), foreignKeyJson(key)), ), ) .toList(); if (add.isNotEmpty) { append( - 'ALTER TABLE ${_quote(after.name)} ${add.map((key) => _mysqlAddForeign(after.name, key)).join(', ')}', + 'ALTER TABLE ${quoteIdentifier(after.name)} ${add.map((key) => mysqlAddForeign(after.name, key)).join(', ')}', before, after, ); diff --git a/lib/src/migrate/mysql_recovery.dart b/lib/src/migrate/mysql_recovery.dart index 271ae289..bce466b7 100644 --- a/lib/src/migrate/mysql_recovery.dart +++ b/lib/src/migrate/mysql_recovery.dart @@ -1,26 +1,28 @@ -part of '../../migrate.dart'; +// MySQL and MariaDB DDL recovery against complete table states. -/// One reviewed MySQL/MariaDB DDL statement and its complete physical table -/// pre/postconditions. DDL is autocommit; each successful step is checkpointed. -/// A null before means create, a null after means drop, differing names rename. -final class CheckedTableSql extends MigrationStep { - final String sql; - final TableSchema? before, after; - CheckedTableSql(this.sql, {this.before, this.after}) { - if (before == null && after == null) { - throw ArgumentError( - 'A checked table step needs a before or after schema.', - ); - } - } - @override - Map toJson() => { - 'kind': 'checkedTableSql', - 'sql': sql, - if (before != null) 'before': _tableJson(before!), - if (after != null) 'after': _tableJson(after!), - }; -} +import '../../driver.dart' show Backend, SqlCommand; +import '../../runtime.dart' show SqlDatabase; +import '../../schema_model.dart' show TableSchema; +import '../../values.dart' show OrmException; +import 'backfill.dart' + show BackfillBudget, BackfillPaused, backfillChunk, verifyBackfill; +import 'catalog.dart' show SchemaVerification, verifySchema; +import 'execute.dart' show executeStep; +import 'history.dart' show Migrator, recordMigration; +import 'migration.dart' show Migration; +import 'mysql_catalog.dart' show mysqlVerifySchema; +import 'recovery.dart' + show + checkpoint, + hasMigrationTable, + loadMigrationProgress, + migrationSession, + savedBackfill; +import 'snapshot.dart' show SchemaSnapshot; +import 'sql_utils.dart' show migrationHash; +import 'sqlite_checks.dart' show sqliteTokens; +import 'step.dart' show Backfill, CheckedTableSql; +import 'validation.dart' show validateMigrations; Future _mysqlExists(SqlDatabase db, String table) async => (await db.execute( @@ -42,11 +44,11 @@ Future _mysqlStateMatches( await _mysqlExists(db, other.name)) { return false; } - final verified = await _mysqlVerifySchema(db, SchemaSnapshot([present])); + final verified = await mysqlVerifySchema(db, SchemaSnapshot([present])); return verified.matches && verified.unmanaged.isEmpty; } -Future _mysqlMigrationLock( +Future mysqlMigrationLock( SqlDatabase session, Duration timeout, Future Function() action, @@ -63,7 +65,7 @@ Future _mysqlMigrationLock( 'Select a MySQL/MariaDB database before migrating.', ); } - final key = 'orm_migrate_${_hash(name).substring(0, 48)}'; + final key = 'orm_migrate_${migrationHash(name).substring(0, 48)}'; Object? primaryFailure; try { final locked = (await session.execute( @@ -109,16 +111,17 @@ Future _mysqlMigrationLock( } } -Future> _applyRecoverableMysql( +Future> applyRecoverableMysql( SqlDatabase session, List migrations, - _BackfillBudget budget, + BackfillBudget budget, ) async { final pending = await Migrator(session).plan(migrations); if (pending.isEmpty) return []; await _mysqlRecoveryTables(session); final progress = { - for (final value in await _progress(session)) (value.id, value.step): value, + for (final value in await loadMigrationProgress(session)) + (value.id, value.step): value, }; final completed = []; for (final migration in pending) { @@ -127,7 +130,7 @@ Future> _applyRecoverableMysql( final step = migration.steps[i]; var phase = 'inspect'; try { - await _checkpoint(session, migration, i, .running, phase); + await checkpoint(session, migration, i, .running, phase); if (step is CheckedTableSql) { final done = await _mysqlStateMatches( session, @@ -142,7 +145,7 @@ Future> _applyRecoverableMysql( ); } phase = 'execute'; - await _checkpoint(session, migration, i, .running, phase); + await checkpoint(session, migration, i, .running, phase); await session.execute(SqlCommand(step.sql)); phase = 'verify'; if (!await _mysqlStateMatches(session, step.after, step.before)) { @@ -153,16 +156,16 @@ Future> _applyRecoverableMysql( } } phase = 'record'; - await _checkpoint(session, migration, i, .complete, 'complete'); + await checkpoint(session, migration, i, .complete, 'complete'); } else if (step is Backfill) { - await _verifyBackfill(session, step); + await verifyBackfill(session, step); while (!await session.transaction( - (tx) async => _backfillChunk( + (tx) async => backfillChunk( tx, migration, i, step, - await _savedBackfill(tx, migration.id, i), + await savedBackfill(tx, migration.id, i), budget, (value) => phase = value, ), @@ -172,14 +175,14 @@ Future> _applyRecoverableMysql( // its checkpoint share the same InnoDB transaction. phase = 'execute'; await session.transaction((tx) async { - await _executeStep(tx, step); - await _checkpoint(tx, migration, i, .complete, 'complete'); + await executeStep(tx, step); + await checkpoint(tx, migration, i, .complete, 'complete'); }); } } catch (error, stack) { - if (error is _BackfillPaused) return completed; + if (error is BackfillPaused) return completed; try { - await _checkpoint( + await checkpoint( session, migration, i, @@ -201,7 +204,7 @@ Future> _applyRecoverableMysql( } } if (migration.snapshot != null) { - final verified = await _mysqlVerifySchema(session, migration.snapshot!); + final verified = await mysqlVerifySchema(session, migration.snapshot!); if (!verified.matches || verified.unmanaged.isNotEmpty) { throw OrmException( 'MIGRATION.DRIFT', @@ -209,7 +212,7 @@ Future> _applyRecoverableMysql( ); } } - await session.transaction((tx) => _recordMigration(tx, migration)); + await session.transaction((tx) => recordMigration(tx, migration)); completed.add(migration.id); } return completed; @@ -232,7 +235,7 @@ Future _mysqlRecoveryTables(SqlDatabase db) async { // history check. Validate the resolved names again after creating the durable // tables, before any application DDL or history/checkpoint writes. for (final table in ['_orm_migrations', '_orm_migration_steps']) { - if (!await _hasMigrationTable(db, table)) { + if (!await hasMigrationTable(db, table)) { throw const OrmException( 'MIGRATION.SESSION', 'Durable migration metadata tables could not be established.', @@ -241,7 +244,7 @@ Future _mysqlRecoveryTables(SqlDatabase db) async { } } -Future _mysqlBaseline( +Future mysqlBaseline( Migrator migrator, List migrations, SchemaSnapshot expected, @@ -261,11 +264,11 @@ Future _mysqlBaseline( 'The final migration must carry the expected baseline snapshot.', ); } - return _migrationSession(migrator.database, migrator.lockTimeout, ( + return migrationSession(migrator.database, migrator.lockTimeout, ( session, ) async { if ((await Migrator(session).history()).isNotEmpty || - (await _progress(session)).isNotEmpty) { + (await loadMigrationProgress(session)).isNotEmpty) { throw const OrmException( 'MIGRATION.BASELINE', 'Migration history already exists.', @@ -278,7 +281,7 @@ Future _mysqlBaseline( await _mysqlRecoveryTables(session); await session.transaction((tx) async { for (final migration in migrations) { - await _recordMigration(tx, migration); + await recordMigration(tx, migration); } }); return verified; @@ -287,8 +290,8 @@ Future _mysqlBaseline( // Migration steps are single reviewed statements. Executable comments may hide // session/DDL operations from the transaction classifier and are not accepted. -void _validateMysqlStatement(String sql) { - final tokens = _sqliteTokens(sql); +void validateMysqlStatement(String sql) { + final tokens = sqliteTokens(sql); var offset = 0; for (var i = 0; i <= tokens.length; i++) { final end = i == tokens.length ? sql.length : tokens[i].start; diff --git a/lib/src/migrate/mysql_schema.dart b/lib/src/migrate/mysql_schema.dart index 1bda04d9..86b0e162 100644 --- a/lib/src/migrate/mysql_schema.dart +++ b/lib/src/migrate/mysql_schema.dart @@ -1,21 +1,30 @@ -part of '../../migrate.dart'; +import '../../driver.dart' show SqlCommand, SqlDialect; +import '../../schema_model.dart' + show CheckSchema, Column, ForeignKey, IndexSchema, TableSchema; +import '../../values.dart' show OrmException; +import 'catalog.dart' show normalizeDefault; +import 'schema.dart' show checkDefinition, foreignKey, validateSchema; +import 'snapshot.dart' show foreignKeyJson; +import 'sql_utils.dart' show migrationHash, quoteIdentifier; +import 'sqlite_checks.dart' show sqliteTokens; +import 'step.dart' show CheckedTableSql, MigrationStep; -bool _isMysql(SqlDialect dialect) => +bool isMysqlFamily(SqlDialect dialect) => dialect == SqlDialect.mysql || dialect == SqlDialect.mariadb; String _mysqlName(String kind, Object signature) => - '_orm_${kind}_${_hash(signature).substring(0, 24)}'; -String _mysqlForeignName(String table, ForeignKey key) => _mysqlName('fk', [ + '_orm_${kind}_${migrationHash(signature).substring(0, 24)}'; +String mysqlForeignName(String table, ForeignKey key) => _mysqlName('fk', [ table, { - ..._foreignKeyJson(key), + ...foreignKeyJson(key), 'onDelete': key.onDelete == 'NO ACTION' ? 'RESTRICT' : key.onDelete, }, ]); -String _mysqlUniqueName(String table, List columns) => +String mysqlUniqueName(String table, List columns) => _mysqlName('uk', [table, columns]); -TableSchema _mysqlCopy( +TableSchema mysqlCopy( TableSchema table, { String? name, List>? columns, @@ -34,7 +43,7 @@ TableSchema _mysqlCopy( checks: checks ?? table.checks, ); -TableSchema _mysqlPhysicalTable(TableSchema table) { +TableSchema mysqlPhysicalTable(TableSchema table) { final indexes = table.indexes.toList(); for (final key in table.foreignKeys) { final candidates = [ @@ -56,10 +65,10 @@ TableSchema _mysqlPhysicalTable(TableSchema table) { ); } } - return _mysqlCopy(table, indexes: indexes); + return mysqlCopy(table, indexes: indexes); } -void _validateMysqlSchema(List tables, SqlDialect dialect) { +void validateMysqlSchema(List tables, SqlDialect dialect) { void distinct(Iterable names) { final seen = {}; for (final name in names) { @@ -141,7 +150,7 @@ void _validateMysqlSchema(List tables, SqlDialect dialect) { } } -String _mysqlStorageType(String type) => switch (type) { +String mysqlStorageType(String type) => switch (type) { 'integer' => 'BIGINT', 'bigint' => 'DECIMAL(65,0)', 'decimal' => 'DECIMAL', @@ -158,7 +167,7 @@ String _mysqlStorageType(String type) => switch (type) { 'No MySQL/MariaDB storage mapping for $type.', ), }; -String _mysqlColumnType(Column column) => +String mysqlColumnType(Column column) => switch (column.codec.sqlType) { 'integer' => switch (column.integerBits ?? 64) { 16 => 'SMALLINT', @@ -171,12 +180,12 @@ String _mysqlColumnType(Column column) => 'instant' || 'timestamp' || 'local_datetime' => 'DATETIME(${column.temporalPrecision ?? 6})', - _ => _mysqlStorageType(column.codec.sqlType), + _ => mysqlStorageType(column.codec.sqlType), }; -String _mysqlColumn(Column column, SqlDialect dialect) { +String mysqlColumn(Column column, SqlDialect dialect) { final result = StringBuffer( - '${_quote(column.name)} ${_mysqlColumnType(column)}', + '${quoteIdentifier(column.name)} ${mysqlColumnType(column)}', ); if (column.codec.sqlType == 'text') { result.write(' CHARACTER SET utf8mb4 COLLATE utf8mb4_bin'); @@ -188,11 +197,11 @@ String _mysqlColumn(Column column, SqlDialect dialect) { } else { result.write(column.nullable ? ' NULL' : ' NOT NULL'); if (column.defaultSql case final value?) { - final sql = _normalizeDefault(value)!; + final sql = normalizeDefault(value)!; // MySQL reparses parenthesized string defaults. With // NO_BACKSLASH_ESCAPES that can change stored backslashes; ordinary // literal defaults also avoid unnecessary expression-default metadata. - final tokens = _sqliteTokens(sql); + final tokens = sqliteTokens(sql); final literal = tokens.length == 1 && (tokens.single.text.startsWith('s:') || @@ -211,55 +220,55 @@ String _mysqlColumn(Column column, SqlDialect dialect) { return result.toString(); } -String _mysqlCreateTable(TableSchema table, SqlDialect dialect) { +String mysqlCreateTable(TableSchema table, SqlDialect dialect) { final definitions = [ - for (final column in table.columns) _mysqlColumn(column, dialect), + for (final column in table.columns) mysqlColumn(column, dialect), if (table.primaryKey.isNotEmpty) - 'PRIMARY KEY (${table.primaryKey.map(_quote).join(', ')})', + 'PRIMARY KEY (${table.primaryKey.map(quoteIdentifier).join(', ')})', for (final key in table.uniqueKeys) - 'CONSTRAINT ${_quote(_mysqlUniqueName(table.name, key))} UNIQUE (${key.map(_quote).join(', ')})', + 'CONSTRAINT ${quoteIdentifier(mysqlUniqueName(table.name, key))} UNIQUE (${key.map(quoteIdentifier).join(', ')})', for (final index in table.indexes) - '${index.unique ? 'UNIQUE ' : ''}INDEX ${_quote(index.name)} (${index.columns.map(_quote).join(', ')})', - for (final check in table.checks) _checkDefinition(check, dialect), + '${index.unique ? 'UNIQUE ' : ''}INDEX ${quoteIdentifier(index.name)} (${index.columns.map(quoteIdentifier).join(', ')})', + for (final check in table.checks) checkDefinition(check, dialect), ]; - return 'CREATE TABLE ${_quote(table.name)} (${definitions.join(', ')}) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin'; + return 'CREATE TABLE ${quoteIdentifier(table.name)} (${definitions.join(', ')}) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin'; } -String _mysqlAddForeign(String table, ForeignKey key) => - 'ADD CONSTRAINT ${_quote(_mysqlForeignName(table, key))} ${_foreignKey(key)}'; +String mysqlAddForeign(String table, ForeignKey key) => + 'ADD CONSTRAINT ${quoteIdentifier(mysqlForeignName(table, key))} ${foreignKey(key)}'; -List _mysqlCreateSchema( +List mysqlCreateSchema( List tables, SqlDialect dialect, ) { - _validateSchema(tables, dialect); - final physical = tables.map(_mysqlPhysicalTable).toList(); + validateSchema(tables, dialect); + final physical = tables.map(mysqlPhysicalTable).toList(); return List.unmodifiable([ - for (final table in physical) SqlCommand(_mysqlCreateTable(table, dialect)), + for (final table in physical) SqlCommand(mysqlCreateTable(table, dialect)), for (final table in physical) if (table.foreignKeys.isNotEmpty) SqlCommand( - 'ALTER TABLE ${_quote(table.name)} ${table.foreignKeys.map((key) => _mysqlAddForeign(table.name, key)).join(', ')}', + 'ALTER TABLE ${quoteIdentifier(table.name)} ${table.foreignKeys.map((key) => mysqlAddForeign(table.name, key)).join(', ')}', ), ]); } -List _mysqlCreateSteps( +List mysqlCreateSteps( List tables, SqlDialect dialect, ) { - final physical = tables.map(_mysqlPhysicalTable).toList(); + final physical = tables.map(mysqlPhysicalTable).toList(); return [ for (final table in physical) CheckedTableSql( - _mysqlCreateTable(table, dialect), - after: _mysqlCopy(table, foreignKeys: []), + mysqlCreateTable(table, dialect), + after: mysqlCopy(table, foreignKeys: []), ), for (final table in physical) if (table.foreignKeys.isNotEmpty) CheckedTableSql( - 'ALTER TABLE ${_quote(table.name)} ${table.foreignKeys.map((key) => _mysqlAddForeign(table.name, key)).join(', ')}', - before: _mysqlCopy(table, foreignKeys: []), + 'ALTER TABLE ${quoteIdentifier(table.name)} ${table.foreignKeys.map((key) => mysqlAddForeign(table.name, key)).join(', ')}', + before: mysqlCopy(table, foreignKeys: []), after: table, ), ]; diff --git a/lib/src/migrate/progress.dart b/lib/src/migrate/progress.dart new file mode 100644 index 00000000..33414ce8 --- /dev/null +++ b/lib/src/migrate/progress.dart @@ -0,0 +1,52 @@ +// Public progress values for recoverable migration steps. + +import 'backfill.dart' show BackfillProgress; + +/// Durable status of one recoverable migration step. +enum MigrationStepState { running, complete, failed } + +/// A recorded checkpoint that allows an interrupted migration to resume safely. +final class MigrationProgress { + /// Identifier of the migration containing this step. + final String id; + + /// Reviewed migration fingerprint stored with this checkpoint. + final String checksum; + + /// Zero-based index in the reviewed migration's steps. + final int step; + + /// Last durable status of this step, distinct from whole-migration completion. + final MigrationStepState state; + + /// Last recorded execution stage, such as scanning, verification, or recording. + final String phase; + + /// Recorded ORM error code or exception type name, or null when none is stored. + final String? failure; + + /// Durable data cursor and counters for a backfill step, when present. + final BackfillProgress? backfill; + + /// Records a step checkpoint for reporting and recovery inspection. + const MigrationProgress( + this.id, + this.checksum, + this.step, + this.state, + this.phase, + this.failure, { + this.backfill, + }); + + /// Structured status for CLI and application reports. + Map toJson() => { + 'id': id, + 'checksum': checksum, + 'step': step, + 'state': state.name, + 'phase': phase, + if (failure != null) 'failure': failure, + if (backfill != null) 'backfill': backfill!.toJson(), + }; +} diff --git a/lib/src/migrate/recovery.dart b/lib/src/migrate/recovery.dart index d8d295ef..30cb0e6a 100644 --- a/lib/src/migrate/recovery.dart +++ b/lib/src/migrate/recovery.dart @@ -1,98 +1,38 @@ -part of '../../migrate.dart'; +// Migration locking, checkpoints, and PostgreSQL/SQLite recovery. -/// PostgreSQL autocommit SQL with explicit recovery conditions. Both checks must -/// return exactly one boolean. They describe durable state, not an object name alone. -final class CheckedSql extends MigrationStep { - final String sql; - final String readyWhen; - final String doneWhen; - const CheckedSql(this.sql, {required this.readyWhen, required this.doneWhen}); +import 'dart:convert' show jsonDecode, jsonEncode, utf8; - /// A concurrent, ascending B-tree index with default collation/opclasses. - /// The completion check compares its definition as well as ready/valid state. - factory CheckedSql.createIndex(String table, IndexSchema index) { - if (index.columns.isEmpty) throw ArgumentError('An index needs columns.'); - final source = _literal(table), name = _literal(index.name); - final columns = 'ARRAY[${index.columns.map(_literal).join(', ')}]::text[]'; - return CheckedSql( - _createIndex(table, index).replaceFirst('INDEX ', 'INDEX CONCURRENTLY '), - readyWhen: '''SELECT NOT EXISTS ( -SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace -WHERE n.nspname = current_schema() AND c.relname = $name)''', - doneWhen: - '''SELECT EXISTS ( -SELECT 1 FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid -JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_class t ON t.oid = i.indrelid -JOIN pg_am am ON am.oid = c.relam -WHERE n.nspname = current_schema() AND c.relname = $name AND t.relname = $source -AND t.relnamespace = n.oid AND i.indisvalid AND i.indisready AND i.indislive -AND i.indisunique = ${index.unique} AND NOT i.indisprimary AND NOT i.indisexclusion -AND i.indexprs IS NULL AND i.indpred IS NULL AND i.indnatts = i.indnkeyatts -AND am.amname = 'btree' AND c.reloptions IS NULL -AND NOT EXISTS (SELECT 1 FROM pg_constraint constraint_row WHERE constraint_row.conindid = i.indexrelid AND constraint_row.contype IN ('p', 'u', 'x')) -AND NOT coalesce((to_jsonb(i)->>'indnullsnotdistinct')::boolean, false) -AND NOT EXISTS (SELECT 1 FROM unnest(i.indoption) v WHERE v <> 0) -AND NOT EXISTS (SELECT 1 FROM unnest(i.indclass) v JOIN pg_opclass o ON o.oid = v WHERE NOT o.opcdefault) -AND NOT EXISTS (SELECT 1 FROM unnest(i.indkey, i.indcollation) k(num, collation_oid) - JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.num WHERE k.collation_oid <> a.attcollation) -AND ARRAY(SELECT a.attname::text FROM unnest(i.indkey) WITH ORDINALITY k(num, ord) - JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.num ORDER BY k.ord) = $columns)''', - ); - } - @override - Map toJson() => { - 'kind': 'checkedSql', - 'sql': sql, - 'readyWhen': readyWhen, - 'doneWhen': doneWhen, - }; -} - -String _literal(String value) { - var tag = r'$orm$'; - while (value.contains(tag)) { - tag = '${tag.substring(0, tag.length - 1)}_\$'; - } - return '$tag$value$tag'; -} - -enum MigrationStepState { running, complete, failed } - -final class MigrationProgress { - final String id; - final String checksum; - final int step; - final MigrationStepState state; - final String phase; - final String? failure; - final BackfillProgress? backfill; - const MigrationProgress( - this.id, - this.checksum, - this.step, - this.state, - this.phase, - this.failure, { - this.backfill, - }); - Map toJson() => { - 'id': id, - 'checksum': checksum, - 'step': step, - 'state': state.name, - 'phase': phase, - if (failure != null) 'failure': failure, - if (backfill != null) 'backfill': backfill!.toJson(), - }; -} +import '../../driver.dart' show Backend, SqlCommand, SqlDialect; +import '../../runtime.dart' + show PostgresTransaction, SqlDatabase, SqliteTransaction; +import '../../values.dart' show OrmException; +import 'backfill.dart' + show + BackfillBudget, + BackfillPaused, + BackfillProgress, + backfillChunk, + parameterMarker, + readBackfillProgress, + verifyBackfill; +import 'execute.dart' show executeStep; +import 'history.dart' show MigrationStatus, Migrator, recordMigration; +import 'migration.dart' show Migration; +import 'mysql_recovery.dart' show mysqlMigrationLock; +import 'mysql_schema.dart' show isMysqlFamily; +import 'progress.dart' show MigrationProgress, MigrationStepState; +import 'sql_utils.dart' show quoteIdentifier; +import 'step.dart' show Backfill, CheckedSql, DropTable, RebuildTable; // The history schema owns these TEXT cells. MySQL can expose a binary-collated // TEXT field as bytes; this does not change decoding of application BLOB values. String? _migrationText(Object? value) => value is List ? utf8.decode(value) : value as String?; -Future> _progress(SqlDatabase db) async { - if (!await _hasMigrationTable(db, '_orm_migration_steps')) { +Future> loadMigrationProgress( + SqlDatabase db, +) async { + if (!await hasMigrationTable(db, '_orm_migration_steps')) { return []; } final result = await db.execute( @@ -129,12 +69,12 @@ Future> _progress(SqlDatabase db) async { _migrationText(row[fields[5]]), backfill: data < 0 || row[data] == null ? null - : BackfillProgress._read(jsonDecode(_migrationText(row[data])!)), + : readBackfillProgress(jsonDecode(_migrationText(row[data])!)), ), ]; } -void _validateProgress( +void validateProgress( List migrations, List applied, List progress, @@ -206,7 +146,7 @@ void _validateProgress( } } -Future _migrationSession( +Future migrationSession( SqlDatabase database, Duration lockTimeout, Future Function(SqlDatabase) action, @@ -215,9 +155,9 @@ Future _migrationSession( throw ArgumentError.value(lockTimeout, 'lockTimeout'); } Future run(SqlDatabase session) async { - await _checkMigrationVersion(session); - return _isMysql(session.dialect) - ? _mysqlMigrationLock(session, lockTimeout, () => action(session)) + await checkMigrationVersion(session); + return isMysqlFamily(session.dialect) + ? mysqlMigrationLock(session, lockTimeout, () => action(session)) : session.dialect == SqlDialect.postgres ? _withMigrationLock(session, lockTimeout, () => action(session)) : action(session); @@ -228,8 +168,8 @@ Future _migrationSession( // Generated PostgreSQL DDL/catalog behavior is supported on 18+. Check the // actual server before locks, journal creation or recoverable partial commits. -Future _checkMigrationVersion(SqlDatabase db) async { - if (_isMysql(db.dialect)) { +Future checkMigrationVersion(SqlDatabase db) async { + if (isMysqlFamily(db.dialect)) { final raw = '${(await db.execute(SqlCommand('SELECT VERSION()'))).rows.single.single}'; final maria = raw.toLowerCase().contains('mariadb'); @@ -330,16 +270,16 @@ Future _withMigrationLock( } } -Future> _applyRecoverable( +Future> applyRecoverable( SqlDatabase session, List migrations, - _BackfillBudget budget, + BackfillBudget budget, ) async { final pending = await Migrator(session).plan(migrations); if (pending.isEmpty) return []; await session.transaction(_recoveryTables); final progress = { - for (final p in await _progress(session)) (p.id, p.step): p, + for (final p in await loadMigrationProgress(session)) (p.id, p.step): p, }; final atomic = []; final completed = []; @@ -348,9 +288,9 @@ Future> _applyRecoverable( await session.transaction((tx) async { for (final migration in atomic) { for (final step in migration.steps) { - await _executeStep(tx, step); + await executeStep(tx, step); } - await _recordMigration(tx, migration); + await recordMigration(tx, migration); } }); completed.addAll(atomic.map((m) => m.id)); @@ -371,34 +311,34 @@ Future> _applyRecoverable( ? 'inspect' : 'execute'; try { - await _checkpoint(session, migration, i, .running, phase); + await checkpoint(session, migration, i, .running, phase); if (step is Backfill) { - await _verifyBackfill(session, step); + await verifyBackfill(session, step); while (!await session.transaction( - (tx) async => _backfillChunk( + (tx) async => backfillChunk( tx, migration, i, step, - await _savedBackfill(tx, migration.id, i), + await savedBackfill(tx, migration.id, i), budget, (value) => phase = value, ), )) {} } else if (step is CheckedSql) { - if (!await _probe(session, step.doneWhen)) { - if (!await _probe(session, step.readyWhen)) { + if (!await probe(session, step.doneWhen)) { + if (!await probe(session, step.readyWhen)) { throw const OrmException( 'MIGRATION.RECOVERY', 'Neither completion nor safe-to-run condition holds. Inspect and repair the database before retrying this unchanged migration.', ); } phase = 'execute'; - await _checkpoint(session, migration, i, .running, phase); + await checkpoint(session, migration, i, .running, phase); await session.execute(SqlCommand(step.sql)); phase = 'verify'; - await _checkpoint(session, migration, i, .running, phase); - if (!await _probe(session, step.doneWhen)) { + await checkpoint(session, migration, i, .running, phase); + if (!await probe(session, step.doneWhen)) { throw const OrmException( 'MIGRATION.POSTCONDITION', 'Nontransactional SQL did not establish its declared completion condition.', @@ -406,18 +346,18 @@ Future> _applyRecoverable( } } phase = 'record'; - await _checkpoint(session, migration, i, .complete, 'complete'); + await checkpoint(session, migration, i, .complete, 'complete'); } else { await session.transaction((tx) async { - await _executeStep(tx, step); - await _checkpoint(tx, migration, i, .complete, 'complete'); + await executeStep(tx, step); + await checkpoint(tx, migration, i, .complete, 'complete'); }); } } catch (error, stack) { - if (error is _BackfillPaused) return completed; + if (error is BackfillPaused) return completed; try { // Never overwrite a completion record after an uncertain commit. - await _checkpoint( + await checkpoint( session, migration, i, @@ -440,20 +380,20 @@ Future> _applyRecoverable( ); } } - await session.transaction((tx) => _recordMigration(tx, migration)); + await session.transaction((tx) => recordMigration(tx, migration)); completed.add(migration.id); } await flush(); return completed; } -bool _needsRebuild(List migrations, SqlDialect dialect) => +bool needsRebuild(List migrations, SqlDialect dialect) => dialect == SqlDialect.sqlite && migrations.any( (m) => m.steps.any((s) => s is RebuildTable || s is DropTable), ); -Future _migrationTransaction( +Future migrationTransaction( SqlDatabase session, Future Function(SqlDatabase) action, { bool rebuild = false, @@ -525,19 +465,19 @@ Future _migrationTransaction( /// SQLite serializes each chunk using BEGIN IMMEDIATE. Re-read durable history /// inside that lock, so other workers may contribute chunks without replaying any. -Future> _applyRecoverableSqlite( +Future> applyRecoverableSqlite( SqlDatabase session, List migrations, - _BackfillBudget budget, + BackfillBudget budget, ) async { - await _migrationTransaction(session, (tx) async { + await migrationTransaction(session, (tx) async { await Migrator(tx).plan(migrations); await _recoveryTables(tx); }); final atomic = [], completed = []; Future flush() async { if (atomic.isEmpty) return; - final applied = await _migrationTransaction(session, (tx) async { + final applied = await migrationTransaction(session, (tx) async { final pending = (await Migrator(tx).plan(migrations)) .map((m) => m.id) .toSet(); @@ -545,13 +485,13 @@ Future> _applyRecoverableSqlite( for (final migration in atomic) { if (!pending.contains(migration.id)) continue; for (final step in migration.steps) { - await _executeStep(tx, step); + await executeStep(tx, step); } - await _recordMigration(tx, migration); + await recordMigration(tx, migration); applied.add(migration.id); } return applied; - }, rebuild: _needsRebuild(atomic, SqlDialect.sqlite)); + }, rebuild: needsRebuild(atomic, SqlDialect.sqlite)); completed.addAll(applied); atomic.clear(); } @@ -567,24 +507,24 @@ Future> _applyRecoverableSqlite( final step = steps[i]; var verified = false, phase = 'inspect'; try { - while (!await _migrationTransaction(session, (tx) async { + while (!await migrationTransaction(session, (tx) async { final pending = await Migrator(tx).plan(migrations); if (!pending.any((m) => m.id == migration.id)) return true; - final current = (await _progress(tx)) + final current = (await loadMigrationProgress(tx)) .where((p) => p.id == migration.id && p.step == i) .firstOrNull; if (current?.state == .complete) return true; if (current == null) { - await _checkpoint(tx, migration, i, .running, 'inspect'); + await checkpoint(tx, migration, i, .running, 'inspect'); return false; } if (step is Backfill) { if (!verified) { phase = 'inspect'; - await _verifyBackfill(tx, step); + await verifyBackfill(tx, step); verified = true; } - return _backfillChunk( + return backfillChunk( tx, migration, i, @@ -595,17 +535,17 @@ Future> _applyRecoverableSqlite( ); } phase = 'execute'; - await _executeStep(tx, step); + await executeStep(tx, step); phase = 'record'; - await _checkpoint(tx, migration, i, .complete, 'complete'); + await checkpoint(tx, migration, i, .complete, 'complete'); return true; }, rebuild: step is RebuildTable || step is DropTable)) {} } catch (error, stack) { - if (error is _BackfillPaused) return completed; + if (error is BackfillPaused) return completed; try { - await _migrationTransaction( + await migrationTransaction( session, - (tx) => _checkpoint( + (tx) => checkpoint( tx, migration, i, @@ -629,12 +569,12 @@ Future> _applyRecoverableSqlite( ); } } - final recorded = await _migrationTransaction(session, (tx) async { + final recorded = await migrationTransaction(session, (tx) async { if (!(await Migrator(tx).plan(migrations)) .any((m) => m.id == migration.id)) { return false; } - await _recordMigration(tx, migration); + await recordMigration(tx, migration); return true; }); if (recorded) completed.add(migration.id); @@ -643,7 +583,7 @@ Future> _applyRecoverableSqlite( return completed; } -Future _probe(SqlDatabase db, String sql) async { +Future probe(SqlDatabase db, String sql) async { final result = await db.execute(SqlCommand(sql)); if (result.rows.length != 1 || result.rows.single.length != 1 || @@ -659,7 +599,7 @@ Future _probe(SqlDatabase db, String sql) async { return result.rows.single.single == true || result.rows.single.single == 1; } -Future _checkpoint( +Future checkpoint( SqlDatabase db, Migration migration, int step, @@ -672,8 +612,8 @@ Future _checkpoint( SqlCommand( ''' INSERT INTO "_orm_migration_steps" (migration_id, checksum, step, state, phase, failure, backfill) -VALUES (${[for (var i = 1; i <= 7; i++) _mark(db.dialect, i)].join(', ')}) -${_isMysql(db.dialect) ? "ON DUPLICATE KEY UPDATE phase = IF(state = 'complete', phase, VALUES(phase)), failure = IF(state = 'complete', failure, VALUES(failure)), backfill = IF(state = 'complete', backfill, COALESCE(VALUES(backfill), backfill)), state = IF(state = 'complete', state, VALUES(state))" : "ON CONFLICT (migration_id, step) DO UPDATE SET state = EXCLUDED.state, phase = EXCLUDED.phase, failure = EXCLUDED.failure, backfill = coalesce(EXCLUDED.backfill, \"_orm_migration_steps\".backfill) WHERE \"_orm_migration_steps\".state <> 'complete'"} ''', +VALUES (${[for (var i = 1; i <= 7; i++) parameterMarker(db.dialect, i)].join(', ')}) +${isMysqlFamily(db.dialect) ? "ON DUPLICATE KEY UPDATE phase = IF(state = 'complete', phase, VALUES(phase)), failure = IF(state = 'complete', failure, VALUES(failure)), backfill = IF(state = 'complete', backfill, COALESCE(VALUES(backfill), backfill)), state = IF(state = 'complete', state, VALUES(state))" : "ON CONFLICT (migration_id, step) DO UPDATE SET state = EXCLUDED.state, phase = EXCLUDED.phase, failure = EXCLUDED.failure, backfill = coalesce(EXCLUDED.backfill, \"_orm_migration_steps\".backfill) WHERE \"_orm_migration_steps\".state <> 'complete'"} ''', [ migration.id, migration.checksum, @@ -688,7 +628,7 @@ ${_isMysql(db.dialect) ? "ON DUPLICATE KEY UPDATE phase = IF(state = 'complete', } Future _recoveryTables(SqlDatabase tx) async { - await tx.execute(SqlCommand(_historyDdl)); + await tx.execute(SqlCommand(historyDdl)); await tx.execute( SqlCommand('''CREATE TABLE IF NOT EXISTS "_orm_migration_steps" ( migration_id TEXT NOT NULL, checksum TEXT NOT NULL, step INTEGER NOT NULL, @@ -705,27 +645,27 @@ Future _recoveryTables(SqlDatabase tx) async { } } -Future _savedBackfill( +Future savedBackfill( SqlDatabase tx, String id, int step, ) async { final row = await tx.execute( SqlCommand( - 'SELECT backfill FROM "_orm_migration_steps" WHERE migration_id = ${_mark(tx.dialect, 1)} AND step = ${_mark(tx.dialect, 2)}', + 'SELECT backfill FROM "_orm_migration_steps" WHERE migration_id = ${parameterMarker(tx.dialect, 1)} AND step = ${parameterMarker(tx.dialect, 2)}', [id, step], ), ); final data = row.rows.single.single; return data == null ? null - : BackfillProgress._read(jsonDecode(_migrationText(data)!)); + : readBackfillProgress(jsonDecode(_migrationText(data)!)); } -const _historyDdl = +const historyDdl = 'CREATE TABLE IF NOT EXISTS "_orm_migrations" (id TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL)'; -Future _hasMigrationTable(SqlDatabase db, String table) async { - if (_isMysql(db.dialect)) { +Future hasMigrationTable(SqlDatabase db, String table) async { + if (isMysqlFamily(db.dialect)) { final rows = await db.execute( SqlCommand( 'SELECT TABLE_TYPE, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', @@ -740,7 +680,7 @@ Future _hasMigrationTable(SqlDatabase db, String table) async { } if (rows.rows.isNotEmpty) { final definition = await db.execute( - SqlCommand('SHOW CREATE TABLE ${_quote(table)}'), + SqlCommand('SHOW CREATE TABLE ${quoteIdentifier(table)}'), ); if ((definition.rows.single[1] as String).toUpperCase().startsWith( 'CREATE TEMPORARY TABLE', diff --git a/lib/src/migrate/schema.dart b/lib/src/migrate/schema.dart index 01c0d06b..d17f963a 100644 --- a/lib/src/migrate/schema.dart +++ b/lib/src/migrate/schema.dart @@ -1,13 +1,37 @@ -part of '../../migrate.dart'; +// Physical schema validation and engine-specific DDL. + +import 'dart:convert' show utf8; + +import '../../driver.dart' show SqlCommand, SqlDialect; +import '../../schema_model.dart' + show + CheckSchema, + Column, + ComputedStorage, + ForeignKey, + IndexSchema, + TableSchema; +import '../../values.dart' show OrmException; +import 'columns.dart' show sqliteCollation; +import 'mysql_schema.dart' + show + isMysqlFamily, + mysqlColumn, + mysqlColumnType, + mysqlCreateSchema, + mysqlStorageType, + validateMysqlSchema; +import 'sql_utils.dart' show quoteIdentifier; +import 'sqlite_checks.dart' show sqliteName; /// Creates a new schema. Applications should execute the resulting SQL through /// reviewed migrations; this does not inspect or mutate an existing database. List createSchema(List tables, SqlDialect dialect) { - if (_isMysql(dialect)) return _mysqlCreateSchema(tables, dialect); + if (isMysqlFamily(dialect)) return mysqlCreateSchema(tables, dialect); final commands = []; - _validateSchema(tables, dialect); + validateSchema(tables, dialect); for (final table in tables) { - commands.add(SqlCommand(_createTable(table, dialect))); + commands.add(SqlCommand(createTable(table, dialect))); } if (dialect == SqlDialect.postgres) { // Creating constraints after all tables also supports cycles and self links. @@ -15,7 +39,7 @@ List createSchema(List tables, SqlDialect dialect) { for (final key in table.foreignKeys) { commands.add( SqlCommand( - 'ALTER TABLE ${_quote(table.name)} ADD ${_foreignKey(key)}', + 'ALTER TABLE ${quoteIdentifier(table.name)} ADD ${foreignKey(key)}', ), ); } @@ -25,8 +49,8 @@ List createSchema(List tables, SqlDialect dialect) { for (final index in table.indexes) { commands.add( SqlCommand( - 'CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${_quote(index.name)} ' - 'ON ${_quote(table.name)} (${index.columns.map(_quote).join(', ')})', + 'CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${quoteIdentifier(index.name)} ' + 'ON ${quoteIdentifier(table.name)} (${index.columns.map(quoteIdentifier).join(', ')})', ), ); } @@ -34,9 +58,9 @@ List createSchema(List tables, SqlDialect dialect) { return List.unmodifiable(commands); } -void _validateSchema(List tables, [SqlDialect? dialect]) { - if (dialect != null && _isMysql(dialect)) { - _validateMysqlSchema(tables, dialect); +void validateSchema(List tables, [SqlDialect? dialect]) { + if (dialect != null && isMysqlFamily(dialect)) { + validateMysqlSchema(tables, dialect); } String identifier(String name) { if (name.isEmpty || @@ -47,7 +71,7 @@ void _validateSchema(List tables, [SqlDialect? dialect]) { 'Identifiers must be non-empty, contain no NUL, and fit PostgreSQL\'s 63-byte limit.', ); } - return dialect == SqlDialect.sqlite ? _sqliteName(name) : name; + return dialect == SqlDialect.sqlite ? sqliteName(name) : name; } final names = {}; @@ -178,7 +202,7 @@ void _validateSchema(List tables, [SqlDialect? dialect]) { } } for (final key in table.foreignKeys) { - _foreignKey(key); + foreignKey(key); identifier(key.target); if (key.columns.toSet().length != key.columns.length || key.targetColumns.toSet().length != key.targetColumns.length || @@ -211,38 +235,42 @@ void _validateSchema(List tables, [SqlDialect? dialect]) { } } -String _createTable(TableSchema table, SqlDialect dialect, {String? name}) { +String createTable(TableSchema table, SqlDialect dialect, {String? name}) { final definitions = []; for (final c in table.columns) { - definitions.add(_columnDefinition(c, dialect)); + definitions.add(columnDefinition(c, dialect)); } if (table.primaryKey.isNotEmpty && !(dialect == SqlDialect.sqlite && table.columns.any((c) => c.generated))) { - definitions.add('PRIMARY KEY (${table.primaryKey.map(_quote).join(', ')})'); + definitions.add( + 'PRIMARY KEY (${table.primaryKey.map(quoteIdentifier).join(', ')})', + ); } for (final key in table.uniqueKeys) { - definitions.add('UNIQUE (${key.map(_quote).join(', ')})'); + definitions.add('UNIQUE (${key.map(quoteIdentifier).join(', ')})'); } - definitions.addAll(table.checks.map((c) => _checkDefinition(c, dialect))); + definitions.addAll(table.checks.map((c) => checkDefinition(c, dialect))); if (dialect == SqlDialect.sqlite) { for (final key in table.foreignKeys) { - definitions.add(_foreignKey(key)); + definitions.add(foreignKey(key)); } } - return 'CREATE TABLE ${_quote(name ?? table.name)} (${definitions.join(', ')})'; + return 'CREATE TABLE ${quoteIdentifier(name ?? table.name)} (${definitions.join(', ')})'; } -String _checkDefinition(CheckSchema check, SqlDialect dialect) => - '${check.name == null ? '' : 'CONSTRAINT ${_quote(check.name!)} '}CHECK (${check.expression(dialect)}\n)'; +String checkDefinition(CheckSchema check, SqlDialect dialect) => + '${check.name == null ? '' : 'CONSTRAINT ${quoteIdentifier(check.name!)} '}CHECK (${check.expression(dialect)}\n)'; -String _columnDefinition(Column c, SqlDialect dialect) { - if (_isMysql(dialect)) return _mysqlColumn(c, dialect); - final b = StringBuffer('${_quote(c.name)} ${_columnStorageType(c, dialect)}'); +String columnDefinition(Column c, SqlDialect dialect) { + if (isMysqlFamily(dialect)) return mysqlColumn(c, dialect); + final b = StringBuffer( + '${quoteIdentifier(c.name)} ${columnStorageType(c, dialect)}', + ); if (dialect == SqlDialect.sqlite && - _sqliteCollation(c.codec.sqlType) != 'binary') { - b.write(' COLLATE "${_sqliteCollation(c.codec.sqlType)}"'); + sqliteCollation(c.codec.sqlType) != 'binary') { + b.write(' COLLATE "${sqliteCollation(c.codec.sqlType)}"'); } if (c.generated) { b.write( @@ -254,37 +282,37 @@ String _columnDefinition(Column c, SqlDialect dialect) { if (!c.nullable) b.write(' NOT NULL'); if (c.computed case final computed?) { b.write( - ' GENERATED ALWAYS AS (${_coerceColumn(computed.expression(dialect), c, dialect)}\n) ${computed.storage.name.toUpperCase()}', + ' GENERATED ALWAYS AS (${coerceColumn(computed.expression(dialect), c, dialect)}\n) ${computed.storage.name.toUpperCase()}', ); } if (c.defaultSql case final value?) { - b.write(' DEFAULT (${_coerceColumn(value, c, dialect)})'); + b.write(' DEFAULT (${coerceColumn(value, c, dialect)})'); } if (dialect == SqlDialect.sqlite && c.temporalPrecision != null && c.temporalPrecision != 6) { b.write( - ' CHECK (${_temporalCheck(c.name, c.codec.sqlType, c.temporalPrecision!)})', + ' CHECK (${temporalCheck(c.name, c.codec.sqlType, c.temporalPrecision!)})', ); } if (dialect == SqlDialect.sqlite && c.decimalPrecision != null) { b.write( - ' CHECK (${_decimalCheck(c.name, c.decimalPrecision!, c.decimalScale ?? 0)})', + ' CHECK (${decimalCheck(c.name, c.decimalPrecision!, c.decimalScale ?? 0)})', ); } if (dialect == SqlDialect.sqlite && c.integerBits != null && c.integerBits != 64) { - b.write(' CHECK (${_integerCheck(c.name, c.integerBits!)})'); + b.write(' CHECK (${integerCheck(c.name, c.integerBits!)})'); } return b.toString(); } -String _createIndex(String table, IndexSchema index) => - 'CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${_quote(index.name)} ' - 'ON ${_quote(table)} (${index.columns.map(_quote).join(', ')})'; +String createIndexSql(String table, IndexSchema index) => + 'CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${quoteIdentifier(index.name)} ' + 'ON ${quoteIdentifier(table)} (${index.columns.map(quoteIdentifier).join(', ')})'; -String _foreignKey(ForeignKey key) { +String foreignKey(ForeignKey key) { if (key.columns.isEmpty || key.columns.length != key.targetColumns.length || !{ @@ -299,12 +327,12 @@ String _foreignKey(ForeignKey key) { 'Invalid foreign key declaration.', ); } - return 'FOREIGN KEY (${key.columns.map(_quote).join(', ')}) REFERENCES ${_quote(key.target)} ' - '(${key.targetColumns.map(_quote).join(', ')}) ON DELETE ${key.onDelete}'; + return 'FOREIGN KEY (${key.columns.map(quoteIdentifier).join(', ')}) REFERENCES ${quoteIdentifier(key.target)} ' + '(${key.targetColumns.map(quoteIdentifier).join(', ')}) ON DELETE ${key.onDelete}'; } -String _storageType(String type, SqlDialect dialect) => _isMysql(dialect) - ? _mysqlStorageType(type) +String _storageType(String type, SqlDialect dialect) => isMysqlFamily(dialect) + ? mysqlStorageType(type) : switch ((dialect, type)) { (SqlDialect.sqlite, 'integer') => 'INTEGER', ( @@ -341,9 +369,9 @@ String _storageType(String type, SqlDialect dialect) => _isMysql(dialect) ), }; -String _columnStorageType(Column column, SqlDialect dialect) => - _isMysql(dialect) - ? _mysqlColumnType(column) +String columnStorageType(Column column, SqlDialect dialect) => + isMysqlFamily(dialect) + ? mysqlColumnType(column) : dialect == SqlDialect.postgres && column.codec.sqlType == 'decimal' && column.decimalPrecision != null @@ -369,14 +397,14 @@ String _columnStorageType(Column column, SqlDialect dialect) => } : _storageType(column.codec.sqlType, dialect); -bool _sameStorage(Column a, Column b) => +bool sameStorage(Column a, Column b) => a.codec.sqlType == b.codec.sqlType && (a.temporalPrecision ?? 6) == (b.temporalPrecision ?? 6) && (a.integerBits ?? 64) == (b.integerBits ?? 64) && a.decimalPrecision == b.decimalPrecision && (a.decimalScale ?? 0) == (b.decimalScale ?? 0); -String _coerceColumn( +String coerceColumn( String expression, Column column, SqlDialect dialect, @@ -388,278 +416,13 @@ String _coerceColumn( ? "orm_temporal_cast_v1($expression, '${column.codec.sqlType}', ${column.temporalPrecision})" : expression; -String _temporalCheck(String name, String kind, int digits) => - "${_quote(name)} IS NULL OR orm_temporal_fits_v1(${_quote(name)}, '$kind', $digits)"; +String temporalCheck(String name, String kind, int digits) => + "${quoteIdentifier(name)} IS NULL OR orm_temporal_fits_v1(${quoteIdentifier(name)}, '$kind', $digits)"; -String _decimalCheck(String name, int precision, int scale) => - '${_quote(name)} IS NULL OR orm_decimal_fits_v1(${_quote(name)}, $precision, $scale)'; +String decimalCheck(String name, int precision, int scale) => + '${quoteIdentifier(name)} IS NULL OR orm_decimal_fits_v1(${quoteIdentifier(name)}, $precision, $scale)'; -String _integerCheck(String name, int bits) { - final column = _quote(name), max = bits == 16 ? 32767 : 2147483647; +String integerCheck(String name, int bits) { + final column = quoteIdentifier(name), max = bits == 16 ? 32767 : 2147483647; return "$column IS NULL OR (typeof($column) = 'integer' AND $column BETWEEN ${-max - 1} AND $max)"; } - -/// Actual catalog columns, rather than a claimed migration version. -final class ColumnInfo { - final String name; - final String storageType; - final bool nullable; - final String? defaultSql; - final bool generated; - final ComputedColumn? computed; - final int? integerBits; - final String? collation; - final int? decimalPrecision; - final int? decimalScale; - final int? temporalPrecision; - const ColumnInfo({ - required this.name, - required this.storageType, - required this.nullable, - this.defaultSql, - this.generated = false, - this.computed, - this.integerBits, - this.collation, - this.decimalPrecision, - this.decimalScale, - this.temporalPrecision, - }); - - /// Removes the managed SQLite default coercion when drafting a declaration. - String? get declarationDefaultSql => - defaultSql != null && storageType == 'TEXT' && decimalPrecision != null - ? _uncoerceDecimalDefault( - _normalizeDefault(defaultSql)!, - decimalPrecision!, - decimalScale ?? 0, - ) ?? - defaultSql - : defaultSql != null && storageType == 'TEXT' && temporalPrecision != null - ? _uncoerceTemporal( - _normalizeDefault(defaultSql)!, - _temporalCollationKind(collation)!, - temporalPrecision!, - ) ?? - defaultSql - : defaultSql; - - /// SQL suitable for a declaration, without ORM storage coercion wrappers. - ComputedColumn? get declarationComputed => _declarationComputed(this); -} - -Future> inspectColumns( - SqlDatabase db, - String table, -) async { - if (_isMysql(db.dialect)) return _mysqlColumns(db, table); - if (db.dialect == SqlDialect.sqlite) { - final rows = await db.execute( - SqlCommand('PRAGMA table_xinfo(${_quote(table)})'), - ); - final indexes = await db.execute( - SqlCommand('PRAGMA index_list(${_quote(table)})'), - ); - final rowidPrimaryKey = !indexes.rows.any((r) => r[3] == 'pk'); - final ddl = await db.execute( - SqlCommand( - "SELECT sql FROM main.sqlite_schema WHERE type = 'table' AND name = ?1", - [table], - ), - ); - final sql = ddl.rows.firstOrNull?.first as String? ?? ''; - final checks = _sqliteChecks(sql); - final computed = _sqliteComputedColumns(sql); - final collations = { - for (final c in _sqliteColumnCollations(sql)) - _sqliteName(c.column): c.collation, - }; - final columns = []; - for (final row in rows.rows) { - final name = row[1] as String; - final type = (row[2] as String).toUpperCase(); - final collation = collations[_sqliteName(name)] ?? 'BINARY'; - final digits = - type == 'TEXT' && collation.toLowerCase() == 'orm_decimal_v1' - ? _sqliteDecimalDigits(name, checks) - : null; - columns.add( - ColumnInfo( - name: name, - storageType: type, - collation: collation, - decimalPrecision: digits?.$1, - decimalScale: digits?.$2, - temporalPrecision: - type == 'TEXT' && _temporalCollationKind(collation) != null - ? _sqliteTemporalPrecision( - name, - _temporalCollationKind(collation)!, - checks, - ) - : null, - nullable: row[3] == 0 && !(row[5] != 0 && rowidPrimaryKey), - defaultSql: row[4] as String?, - generated: (row[6] as int) > 0, - computed: computed[_sqliteName(name)], - integerBits: type == 'INTEGER' - ? _sqliteIntegerBits(name, checks) - : null, - ), - ); - } - return columns; - } - - final result = await db.execute( - SqlCommand( - ''' -SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), - NOT a.attnotnull, pg_get_expr(d.adbin, d.adrelid), - a.attidentity <> '' OR a.attgenerated <> '', a.attgenerated::text -FROM pg_catalog.pg_attribute a -JOIN pg_catalog.pg_class c ON c.oid = a.attrelid -JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace -LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum -WHERE c.relname = \$1 AND n.nspname = current_schema() AND a.attnum > 0 AND NOT a.attisdropped -ORDER BY a.attnum''', - [table], - ), - ); - return [ - for (final row in result.rows) - ColumnInfo( - name: row[0] as String, - storageType: _postgresStorageName(row[1] as String), - temporalPrecision: _postgresTemporalPrecision(row[1] as String), - nullable: row[2] as bool, - defaultSql: row[5] == '' ? row[3] as String? : null, - generated: row[4] as bool, - computed: row[5] == '' - ? null - : ComputedColumn( - row[3] as String, - storage: row[5] == 's' - ? ComputedStorage.stored - : ComputedStorage.virtual, - ), - decimalPrecision: _postgresDecimalDigits(row[1] as String)?.$1, - decimalScale: _postgresDecimalDigits(row[1] as String)?.$2, - integerBits: switch ((row[1] as String).toUpperCase()) { - 'SMALLINT' => 16, - 'INTEGER' => 32, - 'BIGINT' => 64, - _ => null, - }, - ), - ]; -} - -/// Column drift check. Constraints, indexes and unmanaged objects are separate -/// catalog checks; this method does not pretend that columns prove full equality. -Future> verifyColumns( - SqlDatabase db, - List tables, -) async { - final differences = []; - for (final table in tables) { - final inspected = await inspectColumns(db, table.name); - final actual = {for (final c in inspected) c.name: c}; - var contextMatches = true; - for (final expected in table.columns) { - final column = actual.remove(expected.name); - final path = '${table.name}.${expected.name}'; - if (column == null) { - contextMatches = false; - differences.add('$path is missing'); - continue; - } - if (column.storageType != _columnStorageType(expected, db.dialect)) { - contextMatches = false; - differences.add('$path type is ${column.storageType}'); - } - if (column.nullable != expected.nullable) { - differences.add('$path nullability differs'); - } - if ((column.temporalPrecision ?? 6) != - (expected.temporalPrecision ?? 6)) { - differences.add('$path temporal precision differs'); - } - if (!_matchesDecimalDigits(expected, column)) { - differences.add('$path decimal precision/scale differs'); - } - if (db.dialect == SqlDialect.sqlite && - !_matchesCollation(expected, column)) { - differences.add('$path collation differs'); - } - if (expected.codec.sqlType == 'integer' && - (column.integerBits ?? 64) != (expected.integerBits ?? 64)) { - differences.add('$path integer width differs'); - } - } - for (final extra in actual.keys) { - differences.add('${table.name}.$extra is unmanaged'); - } - differences.addAll( - await _verifyComputed( - db, - table, - inspected, - contextMatches: contextMatches, - ), - ); - } - return differences; -} - -bool _matchesCollation(Column expected, ColumnInfo actual) => - (actual.collation ?? 'BINARY').toLowerCase() == - _sqliteCollation(expected.codec.sqlType); - -String _sqliteCollation(String type) => switch (type) { - 'decimal' || - 'date' || - 'time' || - 'local_datetime' || - 'instant' => 'orm_${type}_v1', - _ => 'binary', -}; - -bool _matchesDecimalDigits(Column expected, ColumnInfo actual) => - expected.decimalPrecision == actual.decimalPrecision && - (expected.decimalScale ?? 0) == (actual.decimalScale ?? 0); - -(int, int)? _postgresDecimalDigits(String type) { - final match = RegExp( - r'^numeric\((\d+),(-?\d+)\)$', - caseSensitive: false, - ).firstMatch(type.replaceAll(' ', '')); - return match == null ? null : (int.parse(match[1]!), int.parse(match[2]!)); -} - -String? _temporalCollationKind(String? collation) => - switch (collation?.toLowerCase()) { - 'orm_time_v1' => 'time', - 'orm_local_datetime_v1' => 'local_datetime', - 'orm_instant_v1' => 'instant', - _ => null, - }; - -int? _postgresTemporalPrecision(String type) { - final match = RegExp( - r'^(?:time|timestamp)\(([0-6])\) (?:with|without) time zone$', - caseSensitive: false, - ).firstMatch(type); - return match == null ? null : int.parse(match[1]!); -} - -String _postgresStorageName(String type) { - var name = type.toUpperCase().replaceFirstMapped( - RegExp(r'^TIMESTAMP(\([0-6]\))? WITH TIME ZONE$'), - (m) => 'TIMESTAMPTZ${m[1] ?? ''}', - ); - if (_postgresTemporalPrecision(type) == 6) { - name = name.replaceFirst('(6)', ''); - } - return name; -} diff --git a/lib/src/migrate/snapshot.dart b/lib/src/migrate/snapshot.dart index 839b4d1f..368ae5a2 100644 --- a/lib/src/migrate/snapshot.dart +++ b/lib/src/migrate/snapshot.dart @@ -1,28 +1,48 @@ -part of '../../migrate.dart'; +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' + show + CheckSchema, + Column, + ComputedColumn, + ForeignKey, + IndexSchema, + TableSchema; +import 'computed.dart' show computedJson; +import 'mysql_schema.dart' show isMysqlFamily, mysqlPhysicalTable; +import 'schema.dart' show validateSchema; +import 'sql_utils.dart' show migrationHash; /// Physical schema facts saved as Dart with migrations. Storage codecs support DDL; /// custom domain decoding remains in the generated application client. final class SchemaSnapshot { + /// Immutable physical tables in declaration order. final List tables; + + /// Validates physical metadata without opening a database connection. SchemaSnapshot(List tables) : tables = List.unmodifiable(tables) { - _validateSchema(tables); + validateSchema(tables); } /// Resolves shared declarations to physical facts for this engine only. SchemaSnapshot forDialect(SqlDialect dialect) { - _validateSchema(tables, dialect); - return SchemaSnapshot([for (final t in tables) _targetTable(t, dialect)]); + validateSchema(tables, dialect); + return SchemaSnapshot([for (final t in tables) targetTable(t, dialect)]); } + /// Physical metadata for diagnostic reports and stable fingerprints. + /// + /// Use the Dart source emitters when saving a snapshot with a migration. Map toJson() => { 'format': 1, - 'tables': [for (final table in tables) _tableJson(table)], + 'tables': [for (final table in tables) tableJson(table)], }; - String get checksum => _hash(toJson()); + + /// Stable fingerprint of the physical metadata. + String get checksum => migrationHash(toJson()); } -TableSchema _targetTable(TableSchema table, SqlDialect dialect) { +TableSchema targetTable(TableSchema table, SqlDialect dialect) { final target = TableSchema( table.name, columns: [ @@ -53,7 +73,7 @@ TableSchema _targetTable(TableSchema table, SqlDialect dialect) { for (final c in table.checks) CheckSchema(c.name, c.expression(dialect)), ], ); - return _isMysql(dialect) ? _mysqlPhysicalTable(target) : target; + return isMysqlFamily(dialect) ? mysqlPhysicalTable(target) : target; } Map _checkJson(CheckSchema check) => { @@ -66,13 +86,13 @@ Map _checkJson(CheckSchema check) => { 'mariadb': check.mariadb, }; -Map _columnJson(Column column) => { +Map columnJson(Column column) => { 'name': column.name, 'type': column.codec.sqlType, 'nullable': column.nullable, 'generated': column.generated, if (column.defaultSql != null) 'default': column.defaultSql, - if (column.computed != null) 'computed': _computedJson(column.computed!), + if (column.computed != null) 'computed': computedJson(column.computed!), if (column.integerBits != null && column.integerBits != 64) 'integerBits': column.integerBits, if (column.temporalPrecision != null && column.temporalPrecision != 6) @@ -84,23 +104,23 @@ Map _columnJson(Column column) => { column.decimalScale != 0) 'decimalScale': column.decimalScale, }; -Map _indexJson(IndexSchema index) => { +Map indexJson(IndexSchema index) => { 'name': index.name, 'columns': index.columns, 'unique': index.unique, }; -Map _foreignKeyJson(ForeignKey key) => { +Map foreignKeyJson(ForeignKey key) => { 'columns': key.columns, 'target': key.target, 'targetColumns': key.targetColumns, 'onDelete': key.onDelete, }; -Map _tableJson(TableSchema table) => { +Map tableJson(TableSchema table) => { 'name': table.name, - 'columns': [for (final column in table.columns) _columnJson(column)], + 'columns': [for (final column in table.columns) columnJson(column)], 'primaryKey': table.primaryKey, 'uniqueKeys': table.uniqueKeys, - 'indexes': [for (final index in table.indexes) _indexJson(index)], - 'foreignKeys': [for (final key in table.foreignKeys) _foreignKeyJson(key)], + 'indexes': [for (final index in table.indexes) indexJson(index)], + 'foreignKeys': [for (final key in table.foreignKeys) foreignKeyJson(key)], if (table.checks.isNotEmpty) 'checks': table.checks.map(_checkJson).toList(), }; diff --git a/lib/src/migrate/source.dart b/lib/src/migrate/source.dart index d64b9012..fd1bedc4 100644 --- a/lib/src/migrate/source.dart +++ b/lib/src/migrate/source.dart @@ -1,15 +1,39 @@ -part of '../../migrate.dart'; +import 'dart:convert' show jsonEncode; + +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' + show CheckSchema, Column, ComputedColumn, TableSchema; +import '../../values.dart' show OrmException; +import 'migration.dart' show Migration; +import 'snapshot.dart' show SchemaSnapshot; +import 'step.dart' + show + Backfill, + CheckedSql, + CheckedTableSql, + DropConstraint, + DropTable, + ExecuteSql, + MigrationStep, + RebuildTable; +import 'validation.dart' show validateMigrations; /// Statically imported migrations and their independently recorded fingerprints. /// Keep these entries in version order. [checked] validates before returning them. final class MigrationHistory { + /// The fixed database engine shared by every entry. final SqlDialect dialect; + + /// Migration values paired with their independently reviewed checksums. final List<(Migration, String)> entries; + + /// Copies the ordered entries; [checked] validates their reviewed fingerprints. MigrationHistory( Iterable<(Migration, String)> entries, { required this.dialect, }) : entries = List.unmodifiable(entries); + /// Returns an immutable, validated history or throws on a changed fingerprint. List get checked { final migrations = []; for (final (migration, expected) in entries) { diff --git a/lib/src/migrate/sql_utils.dart b/lib/src/migrate/sql_utils.dart new file mode 100644 index 00000000..24346ac7 --- /dev/null +++ b/lib/src/migrate/sql_utils.dart @@ -0,0 +1,40 @@ +// SQL quoting and stable migration value fingerprints. + +import 'dart:convert' show jsonEncode, utf8; + +import 'package:crypto/crypto.dart' show sha256; + +String quoteIdentifier(String identifier) => + '"${identifier.replaceAll('"', '""')}"'; + +String postgresLiteral(String value) { + var tag = r'$orm$'; + while (value.contains(tag)) { + tag = '${tag.substring(0, tag.length - 1)}_\$'; + } + return '$tag$value$tag'; +} + +Object? canonicalMigrationValue(Object? value) => switch (value) { + Map map => { + for (final key in map.keys.toList()..sort()) + key: canonicalMigrationValue(map[key]), + }, + List list => list.map(canonicalMigrationValue).toList(), + _ => value, +}; + +String migrationHash(Object? value) => sha256 + .convert(utf8.encode(jsonEncode(canonicalMigrationValue(value)))) + .toString(); + +Object? freezeMigrationValue(Object? value) => switch (value) { + Map map => Map.unmodifiable({ + for (final entry in map.entries) + entry.key: freezeMigrationValue(entry.value), + }), + List list => List.unmodifiable( + list.map(freezeMigrationValue), + ), + _ => value, +}; diff --git a/lib/src/migrate/sqlite_checks.dart b/lib/src/migrate/sqlite_checks.dart index 1ce7ece2..ac30eef6 100644 --- a/lib/src/migrate/sqlite_checks.dart +++ b/lib/src/migrate/sqlite_checks.dart @@ -1,7 +1,10 @@ -part of '../../migrate.dart'; +import 'dart:convert' show jsonDecode, jsonEncode; -typedef _SqliteToken = ({String text, int start, int end}); -typedef _SqliteCheck = ({ +import 'columns.dart' show ColumnInfo, temporalCollationKind; +import 'schema.dart' show decimalCheck, integerCheck, temporalCheck; + +typedef SqliteToken = ({String text, int start, int end}); +typedef SqliteCheck = ({ String signature, String expression, String? name, @@ -16,15 +19,15 @@ bool _sqliteWord(int c) => c >= 97 && c <= 122 || c == 95 || c == 36; -String _sqliteName(String name) => String.fromCharCodes( +String sqliteName(String name) => String.fromCharCodes( name.codeUnits.map((c) => c >= 65 && c <= 90 ? c + 32 : c), ); // Tokenize constraint expressions without mistaking quoted defaults/comments for // constraints. Storage ranges still require exact emitted signatures below. // This is not a general SQL parser. -List<_SqliteToken> _sqliteTokens(String sql) { - final result = <_SqliteToken>[]; +List sqliteTokens(String sql) { + final result = []; var i = 0; while (i < sql.length) { if (sql[i].trim().isEmpty) { @@ -79,8 +82,8 @@ List<_SqliteToken> _sqliteTokens(String sql) { return result; } -List<_SqliteCheck> _sqliteChecks(String sql) { - final tokens = _sqliteTokens(sql), checks = <_SqliteCheck>[]; +List sqliteChecks(String sql) { + final tokens = sqliteTokens(sql), checks = []; for (var i = 0; i + 1 < tokens.length; i++) { if (tokens[i].text != 'CHECK' || tokens[i + 1].text != '(') continue; var depth = 1, end = i + 2; @@ -110,10 +113,10 @@ List<_SqliteCheck> _sqliteChecks(String sql) { } String _integerSignature(String name, int bits) => jsonEncode( - _sqliteTokens(_integerCheck(name, bits)).map((t) => t.text).toList(), + sqliteTokens(integerCheck(name, bits)).map((t) => t.text).toList(), ); -int _sqliteIntegerBits(String name, List<_SqliteCheck> checks) { +int sqliteIntegerBits(String name, List checks) { for (final bits in [16, 32]) { final signature = _integerSignature(name, bits); if (checks.any((c) => c.name == null && c.signature == signature)) { @@ -123,12 +126,12 @@ int _sqliteIntegerBits(String name, List<_SqliteCheck> checks) { return 64; } -String _withoutIntegerChecks(String sql, List columns) { +String withoutIntegerChecks(String sql, List columns) { final known = { for (final c in columns.where((c) => c.temporalPrecision != null)) _temporalSignature( c.name, - _temporalCollationKind(c.collation)!, + temporalCollationKind(c.collation)!, c.temporalPrecision!, ), for (final c in columns.where((c) => c.decimalPrecision != null)) @@ -138,7 +141,7 @@ String _withoutIntegerChecks(String sql, List columns) { }; final result = StringBuffer(); var start = 0; - for (final check in _sqliteChecks(sql)) { + for (final check in sqliteChecks(sql)) { if (check.name != null || !known.contains(check.signature)) continue; result.write(sql.substring(start, check.start)); start = check.end; @@ -147,12 +150,12 @@ String _withoutIntegerChecks(String sql, List columns) { } String _decimalSignature(String name, int precision, int scale) => jsonEncode( - _sqliteTokens(_decimalCheck(name, precision, scale)) + sqliteTokens(decimalCheck(name, precision, scale)) .map((t) => t.text) .toList(), ); -(int, int)? _sqliteDecimalDigits(String name, List<_SqliteCheck> checks) { +(int, int)? sqliteDecimalDigits(String name, List checks) { for (final check in checks) { if (check.name != null) continue; final tokens = (jsonDecode(check.signature) as List).cast(); @@ -177,8 +180,8 @@ String _decimalSignature(String name, int precision, int scale) => jsonEncode( return null; } -String? _uncoerceDecimalDefault(String sql, int precision, int scale) { - final tokens = _sqliteTokens(sql); +String? uncoerceDecimalDefault(String sql, int precision, int scale) { + final tokens = sqliteTokens(sql); if (tokens.length < 8 || tokens[0].text != 'ORM_DECIMAL_CAST_V1' || tokens[1].text != '(' || @@ -206,7 +209,7 @@ String? _uncoerceDecimalDefault(String sql, int precision, int scale) { return sql.substring(tokens[2].start, tokens[a].start).trim(); } -typedef _SqliteCollation = ({ +typedef SqliteCollation = ({ String column, String collation, int start, @@ -215,8 +218,8 @@ typedef _SqliteCollation = ({ // Only column-level clauses at the CREATE TABLE body's outer depth. COLLATE // inside CHECK/default expressions and table/index constraints is not erased. -List<_SqliteCollation> _sqliteColumnCollations(String sql) { - final tokens = _sqliteTokens(sql), result = <_SqliteCollation>[]; +List sqliteColumnCollations(String sql) { + final tokens = sqliteTokens(sql), result = []; var depth = 0; String? column; var beginning = false; @@ -262,15 +265,15 @@ List<_SqliteCollation> _sqliteColumnCollations(String sql) { return result; } -String _withoutStorageCollations(String sql, List columns) { +String withoutStorageCollations(String sql, List columns) { final names = { for (final c in columns.where((c) => c.storageType == 'TEXT')) - _sqliteName(c.name), + sqliteName(c.name), }; final result = StringBuffer(); var start = 0; - for (final c in _sqliteColumnCollations(sql)) { - if (!names.contains(_sqliteName(c.column)) || + for (final c in sqliteColumnCollations(sql)) { + if (!names.contains(sqliteName(c.column)) || !{ 'orm_decimal_v1', 'orm_date_v1', @@ -287,12 +290,12 @@ String _withoutStorageCollations(String sql, List columns) { } String _temporalSignature(String name, String kind, int digits) => jsonEncode( - _sqliteTokens(_temporalCheck(name, kind, digits)).map((t) => t.text).toList(), + sqliteTokens(temporalCheck(name, kind, digits)).map((t) => t.text).toList(), ); -int? _sqliteTemporalPrecision( +int? sqliteTemporalPrecision( String name, String kind, - List<_SqliteCheck> checks, + List checks, ) { for (var digits = 0; digits < 6; digits++) { final signature = _temporalSignature(name, kind, digits); @@ -303,8 +306,8 @@ int? _sqliteTemporalPrecision( return null; } -String? _uncoerceTemporal(String sql, String kind, int digits) { - final tokens = _sqliteTokens(sql); +String? uncoerceTemporal(String sql, String kind, int digits) { + final tokens = sqliteTokens(sql); if (tokens.length < 8 || tokens[0].text != 'ORM_TEMPORAL_CAST_V1' || tokens[1].text != '(' || diff --git a/lib/src/migrate/step.dart b/lib/src/migrate/step.dart index 10787b04..75025a51 100644 --- a/lib/src/migrate/step.dart +++ b/lib/src/migrate/step.dart @@ -1,18 +1,40 @@ -part of '../../migrate.dart'; +// Reviewed operation definitions. All subtypes stay with the sealed base. + +import '../../schema_model.dart' show IndexSchema, TableSchema; +import '../../values.dart' show OrmException; +import 'schema.dart' show createIndexSql; +import 'snapshot.dart' show SchemaSnapshot, tableJson; +import 'sql_utils.dart' show freezeMigrationValue, postgresLiteral; +import 'validation.dart' show sqlWords; /// An explicit operation in a reviewed migration. sealed class MigrationStep { const MigrationStep(); + + /// Stable operation data used in migration checksums and reports. Map toJson(); } -final class ExecuteSql(final String sql) extends MigrationStep { +/// One trusted SQL statement executed by the migration's transaction runner. +final class ExecuteSql( + /// Trusted SQL statement whose exact text participates in the checksum. + final String sql, +) extends MigrationStep { + /// Prepares one statement for transactional migration execution. + this; + @override Map toJson() => {'kind': 'sql', 'sql': sql}; } /// A reviewed table removal. The SQLite runner checks foreign keys before commit. -final class DropTable(final String table) extends MigrationStep { +final class DropTable( + /// Physical table name to remove; the runner quotes it as an identifier. + final String table, +) extends MigrationStep { + /// Records a reviewed table removal without executing it. + this; + @override Map toJson() => {'kind': 'dropTable', 'table': table}; } @@ -20,9 +42,18 @@ final class DropTable(final String table) extends MigrationStep { /// SQLite's copy-and-replace operation. Expressions are trusted migration SQL. /// Both snapshots are after any explicit table/column renames. final class RebuildTable extends MigrationStep { + /// Reviewed table shape before copying rows. final TableSchema before; + + /// Reviewed replacement shape after copying rows. final TableSchema after; + + /// Target column names mapped to trusted SQL expressions over the old row. final Map copy; + + /// Freezes an explicit copy map between two shapes of the same SQLite table. + /// + /// The map must be nonempty and may target only existing, noncomputed columns. RebuildTable(this.before, this.after, {required Map copy}) : copy = Map.unmodifiable(copy) { if (before.name != after.name || @@ -40,8 +71,8 @@ final class RebuildTable extends MigrationStep { @override Map toJson() => { 'kind': 'rebuild', - 'before': _tableJson(before), - 'after': _tableJson(after), + 'before': tableJson(before), + 'after': tableJson(after), 'copy': copy, }; } @@ -49,10 +80,15 @@ final class RebuildTable extends MigrationStep { /// Resolves the actual PostgreSQL name by a constraint signature. This also /// works for baselined databases whose constraint names were chosen elsewhere. final class DropConstraint extends MigrationStep { + /// Physical PostgreSQL table whose matching constraint will be removed. final String table; + + /// Immutable constraint signature used to find its actual catalog name. final Map constraint; + + /// Freezes a reviewed signature; execution requires exactly one catalog match. DropConstraint(this.table, Map constraint) - : constraint = _freezeJson(constraint) as Map; + : constraint = freezeMigrationValue(constraint) as Map; @override Map toJson() => { 'kind': 'dropConstraint', @@ -61,186 +97,165 @@ final class DropConstraint extends MigrationStep { }; } -Future _executeStep(SqlDatabase db, MigrationStep step) async { - switch (step) { - case CheckedSql() || CheckedTableSql() || Backfill(): - throw const OrmException( - 'MIGRATION.TRANSACTION', - 'This step requires the recovery runner.', - ); - case ExecuteSql(): - await db.execute(SqlCommand(step.sql)); - case DropTable(): - await db.execute(SqlCommand('DROP TABLE ${_quote(step.table)}')); - case RebuildTable(): - await _rebuild(db, step); - case DropConstraint(): - if (step.constraint['kind'] == 'c') { - await _dropCheck( - db, - step.table, - CheckSchema( - step.constraint['name'] as String?, - step.constraint['expression'] as String, - ), - ); - return; - } - final rows = await db.execute( - SqlCommand( - r''' -SELECT c.conname, c.contype::text, - ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY k(num, ord) - JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.num ORDER BY k.ord), - t.relname, - ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY k(num, ord) - JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.num ORDER BY k.ord), - c.confdeltype::text -FROM pg_constraint c JOIN pg_class r ON r.oid = c.conrelid -JOIN pg_namespace n ON n.oid = r.relnamespace -LEFT JOIN pg_class t ON t.oid = c.confrelid -WHERE n.nspname = current_schema() AND r.relname = $1''', - [step.table], - ), - ); - final matches = rows.rows - .where( - (row) => - _hash({ - 'kind': row[1], - 'columns': row[2], - if (row[1] == 'f') ...{ - 'target': row[3], - 'targetColumns': row[4], - 'onDelete': switch (row[5]) { - 'a' => 'NO ACTION', - 'r' => 'RESTRICT', - 'c' => 'CASCADE', - 'n' => 'SET NULL', - 'd' => 'SET DEFAULT', - _ => '', - }, - }, - }) == - _hash(step.constraint), - ) - .toList(); - if (matches.length != 1) { - throw OrmException( - 'MIGRATION.DRIFT', - 'Expected exactly one matching constraint on ${step.table}; found ${matches.length}.', - ); - } - await db.execute( - SqlCommand( - 'ALTER TABLE ${_quote(step.table)} DROP CONSTRAINT ${_quote(matches.single.first as String)}', - ), - ); - } -} +/// PostgreSQL autocommit SQL with explicit recovery conditions. Both checks must +/// return exactly one boolean. They describe durable state, not an object name alone. +final class CheckedSql extends MigrationStep { + /// Trusted PostgreSQL statement executed outside a transaction. + final String sql; -Future _rebuild(SqlDatabase db, RebuildTable step) async { - final before = step.before, after = step.after, name = after.name; - final actual = await inspectTable(db, name); - final checkMatches = await _matchChecks( - db, - name, - before.checks, - actual.checks, - ); - if (actual.checks.length > checkMatches.whereType().length) { - throw OrmException( - 'MIGRATION.UNMANAGED', - '$name has undeclared CHECK constraints.', - ); - } - final unsupported = actual.unmanaged - .where((o) => !{'index', 'trigger', 'view'}.contains(o.kind)) - .toList(); - if (unsupported.isNotEmpty || - actual.columns.any((c) => c.generated && c.computed == null)) { - throw OrmException( - 'MIGRATION.UNMANAGED', - '$name has unmodeled table constraints/options; provide a manual migration to preserve them.', + /// SELECT returning one boolean that confirms execution is safe to start. + final String readyWhen; + + /// SELECT returning one boolean that confirms the durable result. + final String doneWhen; + + /// Pairs autocommit SQL with explicit readiness and completion probes. + const CheckedSql(this.sql, {required this.readyWhen, required this.doneWhen}); + + /// A concurrent, ascending B-tree index with default collation/opclasses. + /// The completion check compares its definition as well as ready/valid state. + factory CheckedSql.createIndex(String table, IndexSchema index) { + if (index.columns.isEmpty) throw ArgumentError('An index needs columns.'); + final source = postgresLiteral(table), name = postgresLiteral(index.name); + final columns = + 'ARRAY[${index.columns.map(postgresLiteral).join(', ')}]::text[]'; + return CheckedSql( + createIndexSql( + table, + index, + ).replaceFirst('INDEX ', 'INDEX CONCURRENTLY '), + readyWhen: '''SELECT NOT EXISTS ( +SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = current_schema() AND c.relname = $name)''', + doneWhen: + '''SELECT EXISTS ( +SELECT 1 FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid +JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_class t ON t.oid = i.indrelid +JOIN pg_am am ON am.oid = c.relam +WHERE n.nspname = current_schema() AND c.relname = $name AND t.relname = $source +AND t.relnamespace = n.oid AND i.indisvalid AND i.indisready AND i.indislive +AND i.indisunique = ${index.unique} AND NOT i.indisprimary AND NOT i.indisexclusion +AND i.indexprs IS NULL AND i.indpred IS NULL AND i.indnatts = i.indnkeyatts +AND am.amname = 'btree' AND c.reloptions IS NULL +AND NOT EXISTS (SELECT 1 FROM pg_constraint constraint_row WHERE constraint_row.conindid = i.indexrelid AND constraint_row.contype IN ('p', 'u', 'x')) +AND NOT coalesce((to_jsonb(i)->>'indnullsnotdistinct')::boolean, false) +AND NOT EXISTS (SELECT 1 FROM unnest(i.indoption) v WHERE v <> 0) +AND NOT EXISTS (SELECT 1 FROM unnest(i.indclass) v JOIN pg_opclass o ON o.oid = v WHERE NOT o.opcdefault) +AND NOT EXISTS (SELECT 1 FROM unnest(i.indkey, i.indcollation) k(num, collation_oid) + JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.num WHERE k.collation_oid <> a.attcollation) +AND ARRAY(SELECT a.attname::text FROM unnest(i.indkey) WITH ORDINALITY k(num, ord) + JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.num ORDER BY k.ord) = $columns)''', ); } - final check = await verifySchema(db, SchemaSnapshot([before])); - final differences = check.differences - .where((d) => d != '$name indexes differs') - .toList(); - for (final index in before.indexes) { - if (!actual.indexes.any( - (i) => _hash(_indexJson(i)) == _hash(_indexJson(index)), - )) { - differences.add('$name.${index.name} index differs'); + @override + Map toJson() => { + 'kind': 'checkedSql', + 'sql': sql, + 'readyWhen': readyWhen, + 'doneWhen': doneWhen, + }; +} + +/// One reviewed MySQL/MariaDB DDL statement and its complete physical table +/// pre/postconditions. DDL is autocommit; each successful step is checkpointed. +/// A null before means create, a null after means drop, differing names rename. +final class CheckedTableSql extends MigrationStep { + /// One trusted MySQL/MariaDB DDL statement executed with recovery checkpoints. + final String sql; + + /// Reviewed states before and after DDL; null denotes absence on that side. + /// + /// Null [before] describes creation, null [after] describes removal, and + /// different names describe a rename. At least one state must be present. + final TableSchema? before, after; + + /// Records DDL with table-state conditions instead of assuming DDL is atomic. + CheckedTableSql(this.sql, {this.before, this.after}) { + if (before == null && after == null) { + throw ArgumentError( + 'A checked table step needs a before or after schema.', + ); } } - if (differences.isNotEmpty) { - throw OrmException('MIGRATION.DRIFT', differences.join('\n')); - } - final removed = before.columns - .map((c) => c.name) - .toSet() - .difference(after.columns.map((c) => c.name).toSet()); - if (removed.isNotEmpty && actual.unmanaged.any((o) => o.kind == 'trigger')) { - throw OrmException( - 'MIGRATION.UNMANAGED', - '$name has triggers and removed columns; update them in a manual migration.', - ); - } - final objects = await db.execute( - SqlCommand( - "SELECT type, name, sql FROM sqlite_schema WHERE tbl_name = ?1 AND type IN ('index', 'trigger') AND sql IS NOT NULL ORDER BY type, name", - [name], - ), - ); - final managedIndexes = before.indexes.map((i) => i.name).toSet(); - final targetIndexes = after.indexes.map((i) => i.name).toSet(); - for (final row in objects.rows) { - if (row[0] == 'index' && - !managedIndexes.contains(row[1]) && - targetIndexes.contains(row[1])) { - throw OrmException( - 'MIGRATION.UNMANAGED', - 'New index ${row[1]} conflicts with an unmanaged index.', + @override + Map toJson() => { + 'kind': 'checkedTableSql', + 'sql': sql, + if (before != null) 'before': tableJson(before!), + if (after != null) 'after': tableJson(after!), + }; +} + +/// A reviewed, bounded update using this migration's historical table definition. +/// Expressions and completion SQL are trusted migration code. Primary keys must +/// remain immutable, including in application writes and triggers. +final class Backfill extends MigrationStep { + /// Frozen physical table definition from this migration. + final TableSchema table; + + /// Non-key target columns mapped to trusted SQL expressions. + final Map set; + + /// Trusted SQL predicate selecting rows that still need an update. + final String where; + + /// SELECT returning one boolean to verify completion after scanning. + final String doneWhen; + + /// Maximum rows updated in each committed batch. + final int batchSize; + + /// Prepares bounded updates over an immutable, non-null historical primary key. + /// + /// Assignments must be nonempty, target writable non-key columns, and use + /// trusted SQL. [batchSize] must be positive; [doneWhen] must be a SELECT probe. + Backfill( + this.table, { + required Map set, + this.where = 'TRUE', + required this.doneWhen, + this.batchSize = 1000, + }) : set = Map.unmodifiable(set) { + SchemaSnapshot([table]); + if (batchSize < 1 || + set.isEmpty || + where.trim().isEmpty || + set.entries.any( + (e) => + e.value.trim().isEmpty || + !table.columns.any( + (c) => c.name == e.key && !c.generated && c.computed == null, + ) || + table.primaryKey.contains(e.key), + )) { + throw ArgumentError( + 'Backfill needs a positive batch size and assignments to existing non-key columns.', + ); + } + if (table.primaryKey.isEmpty || + table.columns + .where((c) => table.primaryKey.contains(c.name)) + .any((c) => c.nullable || c.codec.sqlType == 'json')) { + throw const OrmException( + 'MIGRATION.BACKFILL_KEY', + 'Backfill requires a non-null primary key with lossless cursor storage; JSON keys are unsupported.', + ); + } + if (sqlWords(doneWhen).firstOrNull != 'SELECT') { + throw const OrmException( + 'MIGRATION.PROBE', + 'Backfill completion must be a SELECT returning one boolean.', ); } } - final temporary = '_orm_rebuild_$name'; - await db.execute( - SqlCommand(_createTable(after, SqlDialect.sqlite, name: temporary)), - ); - await db.execute( - SqlCommand( - 'INSERT INTO ${_quote(temporary)} (${step.copy.keys.map(_quote).join(', ')}) ' - 'SELECT ${step.copy.entries.map((e) => _coerceColumn(e.value, after.columns.firstWhere((c) => c.name == e.key), db.dialect)).join(', ')} FROM ${_quote(name)}', - ), - ); - await db.execute(SqlCommand('DROP TABLE ${_quote(name)}')); - // Views remain present while their source is temporarily absent. Legacy mode - // prevents SQLite from reparsing those views during this one rename. - final legacy = (await db.execute(SqlCommand('PRAGMA legacy_alter_table'))) - .rows - .single - .single; - try { - await db.execute(SqlCommand('PRAGMA legacy_alter_table = ON')); - await db.execute( - SqlCommand('ALTER TABLE ${_quote(temporary)} RENAME TO ${_quote(name)}'), - ); - } finally { - await db.execute( - SqlCommand('PRAGMA legacy_alter_table = ${legacy == 1 ? 'ON' : 'OFF'}'), - ); - } - for (final index in after.indexes) { - await db.execute(SqlCommand(_createIndex(name, index))); - } - for (final row in objects.rows) { - if (row[0] == 'index' && managedIndexes.contains(row[1])) continue; - await db.execute(SqlCommand(row[2] as String)); - } - for (final view in actual.unmanaged.where((o) => o.kind == 'view')) { - await db.execute(SqlCommand('SELECT * FROM ${_quote(view.name)} LIMIT 0')); - } + + @override + Map toJson() => { + 'kind': 'backfill', + 'table': tableJson(table), + 'set': set, + 'where': where, + 'doneWhen': doneWhen, + 'batchSize': batchSize, + }; } diff --git a/lib/src/migrate/validation.dart b/lib/src/migrate/validation.dart new file mode 100644 index 00000000..1b4523a2 --- /dev/null +++ b/lib/src/migrate/validation.dart @@ -0,0 +1,236 @@ +// Validation of immutable histories and executable migration statements. + +import '../../driver.dart' show SqlDialect; +import '../../schema_model.dart' show TableSchema; +import '../../values.dart' show OrmException; +import 'migration.dart' show Migration; +import 'mysql_recovery.dart' show validateMysqlStatement; +import 'mysql_schema.dart' show isMysqlFamily; +import 'schema.dart' show validateSchema; +import 'step.dart' + show + Backfill, + CheckedSql, + CheckedTableSql, + DropConstraint, + DropTable, + ExecuteSql, + RebuildTable; + +/// Validates local ordering, checksum links and operations without opening a database. +void validateMigrations( + List migrations, { + required SqlDialect dialect, +}) { + String? last; + String? previousChecksum; + for (final migration in migrations) { + if (last != null && last.compareTo(migration.id) >= 0) { + throw const OrmException( + 'MIGRATION.ORDER', + 'Migration IDs must be unique and strictly ordered.', + ); + } + last = migration.id; + if (migration.previous != null && migration.previous != previousChecksum) { + throw const OrmException( + 'MIGRATION.CHAIN', + 'Migration previous-checksum chain is broken.', + ); + } + previousChecksum = migration.checksum; + if (migration.dialect != dialect) { + throw OrmException( + 'MIGRATION.TARGET', + '${migration.id} targets ${migration.dialect.name}, but this history requires ${dialect.name}.', + ); + } + if (isMysqlFamily(dialect) && migration.id.length > 191) { + throw const OrmException( + 'MIGRATION.ID', + 'MySQL migration IDs must not exceed 191 characters.', + ); + } + for (final step in migration.steps) { + if (step is CheckedTableSql) { + if (!isMysqlFamily(dialect)) { + throw const OrmException( + 'MIGRATION.TARGET', + 'CheckedTableSql requires MySQL or MariaDB.', + ); + } + for (final table in [ + step.before, + step.after, + ].whereType()) { + validateSchema([table], dialect); + } + _validateSql(step.sql, transactional: false); + validateMysqlStatement(step.sql); + if (!{ + 'CREATE', + 'ALTER', + 'DROP', + 'RENAME', + }.contains(sqlWords(step.sql).first)) { + throw const OrmException( + 'MIGRATION.DDL', + 'CheckedTableSql must contain a reviewed table DDL statement.', + ); + } + continue; + } + if (step is Backfill) { + validateSchema([step.table], dialect); + continue; + } + if (step is CheckedSql) { + if (dialect != SqlDialect.postgres) { + throw const OrmException( + 'MIGRATION.TARGET', + 'Recoverable autocommit migrations currently require PostgreSQL.', + ); + } + _validateSql(step.sql, transactional: false); + for (final probe in [step.readyWhen, step.doneWhen]) { + if (sqlWords(probe).firstOrNull != 'SELECT') { + throw const OrmException( + 'MIGRATION.PROBE', + 'Recovery conditions must be SELECT queries.', + ); + } + } + continue; + } + if (step is DropTable) { + if (isMysqlFamily(dialect)) { + throw const OrmException( + 'MIGRATION.RECOVERY', + 'MySQL table removal requires CheckedTableSql with its before schema.', + ); + } + continue; + } + if (step is RebuildTable) { + if (dialect != SqlDialect.sqlite) { + throw const OrmException( + 'MIGRATION.TARGET', + 'Table rebuilds are SQLite operations.', + ); + } + validateSchema([step.before], dialect); + validateSchema([step.after], dialect); + continue; + } + if (step is DropConstraint) { + if (dialect != SqlDialect.postgres) { + throw const OrmException( + 'MIGRATION.TARGET', + 'Constraint resolution is a PostgreSQL operation.', + ); + } + continue; + } + final sql = (step as ExecuteSql).sql; + _validateSql(sql, transactional: true); + if (isMysqlFamily(dialect)) { + validateMysqlStatement(sql); + } + if (isMysqlFamily(dialect) && + !{ + 'INSERT', + 'UPDATE', + 'DELETE', + 'REPLACE', + }.contains(sqlWords(sql).first)) { + throw const OrmException( + 'MIGRATION.RECOVERY', + 'MySQL ExecuteSql accepts DML only. DDL requires CheckedTableSql with reviewed before/after schemas.', + ); + } + } + } +} + +void _validateSql(String sql, {required bool transactional}) { + final words = sqlWords(sql); + if (words.isEmpty) { + throw const OrmException( + 'MIGRATION.EMPTY', + 'Migration contains an empty statement.', + ); + } + if ({ + 'BEGIN', + 'COMMIT', + 'ROLLBACK', + 'END', + 'SAVEPOINT', + 'RELEASE', + 'PRAGMA', + 'START', + 'ABORT', + }.contains(words.first) || + (transactional && + (words.first == 'VACUUM' || words.contains('CONCURRENTLY')))) { + throw const OrmException( + 'MIGRATION.TRANSACTION', + 'Transaction control belongs to the runner; autocommit operations need CheckedSql.', + ); + } +} + +List sqlWords(String sql) { + final words = []; + var i = 0; + while (i < sql.length) { + if (sql.startsWith('--', i)) { + final end = sql.indexOf('\n', i + 2); + i = end < 0 ? sql.length : end + 1; + } else if (sql.startsWith('/*', i)) { + var depth = 1; + i += 2; + while (i < sql.length && depth > 0) { + if (sql.startsWith('/*', i)) { + depth++; + i += 2; + } else if (sql.startsWith('*/', i)) { + depth--; + i += 2; + } else { + i++; + } + } + } else if (sql[i] == "'" || sql[i] == '"') { + final quote = sql[i++]; + while (i < sql.length) { + if (sql[i++] == quote) { + if (i < sql.length && sql[i] == quote) { + i++; + } else { + break; + } + } + } + } else if (sql[i] == r'$') { + final tag = RegExp(r'^\$[a-zA-Z0-9_]*\$') + .firstMatch(sql.substring(i)) + ?.group(0); + if (tag == null) { + i++; + } else { + final end = sql.indexOf(tag, i + tag.length); + i = end < 0 ? sql.length : end + tag.length; + } + } else if (RegExp(r'[a-zA-Z_]').hasMatch(sql[i])) { + final start = i++; + while (i < sql.length && RegExp(r'[a-zA-Z0-9_]').hasMatch(sql[i])) { + i++; + } + words.add(sql.substring(start, i).toUpperCase()); + } else { + i++; + } + } + return words; +} diff --git a/lib/src/mysql/connection.dart b/lib/src/mysql/connection.dart index 36ab5096..7dd1f841 100644 --- a/lib/src/mysql/connection.dart +++ b/lib/src/mysql/connection.dart @@ -1,14 +1,23 @@ -part of '../../drivers/mysql.dart'; +import 'dart:async'; +import 'dart:io'; -final class _MysqlConnection implements SqlConnection { +import 'package:mysql_client_plus/exception.dart' as mysql; +import 'package:mysql_client_plus/mysql_client_plus.dart' as mysql; + +import '../../driver.dart'; +import 'failure.dart'; +import 'values.dart'; + +final class MysqlConnection implements SqlConnection { final mysql.MySQLConnection _connection; final Duration _queryTimeout; bool _active = true; bool _invalid = false; + bool get isInvalid => _invalid; Future? _pending; Future? _releasing; - _MysqlConnection(this._connection, this._queryTimeout); + MysqlConnection(this._connection, this._queryTimeout); // The upstream client discards OK/EOF status flags. Do not infer server // transaction state from the last SQL string or from a client-side bool. @@ -45,7 +54,7 @@ final class _MysqlConnection implements SqlConnection { ), ); } - final parameters = command.parameters.map(_mysqlParameter).toList(); + final parameters = command.parameters.map(mysqlParameter).toList(); late final Future operation; operation = _execute(command.sql, parameters) .timeout( @@ -94,7 +103,7 @@ final class _MysqlConnection implements SqlConnection { for (final row in result.rows) [ for (var i = 0; i < columns.length; i++) - _mysqlValue( + mysqlValue( row.colAt(i), columns[i].type.intVal, binary: parameters.isNotEmpty, @@ -102,8 +111,8 @@ final class _MysqlConnection implements SqlConnection { ], ], columns: [for (final column in columns) column.name], - affectedRows: _checkedMysqlInt(result.affectedRows), - lastInsertId: lastId == BigInt.zero ? null : _checkedMysqlInt(lastId), + affectedRows: checkedMysqlInt(result.affectedRows), + lastInsertId: lastId == BigInt.zero ? null : checkedMysqlInt(lastId), ); } on mysql.MySQLServerException catch (error) { throw MysqlFailure(error); @@ -155,7 +164,7 @@ final class _MysqlConnection implements SqlConnection { if (_active || _invalid) _discard(); } - Future _release() => _releasing ??= (() async { + Future release() => _releasing ??= (() async { _active = false; try { await _pending; diff --git a/lib/src/mysql/driver.dart b/lib/src/mysql/driver.dart new file mode 100644 index 00000000..3dce26ca --- /dev/null +++ b/lib/src/mysql/driver.dart @@ -0,0 +1,229 @@ +import 'dart:async'; + +import 'package:mysql_client_plus/mysql_client_plus.dart' as mysql; + +import '../../driver.dart'; +import 'connection.dart'; +import 'options.dart'; + +/// One physical connection. Whole leases are queued, including transactions. +/// Create additional drivers explicitly when independent concurrent leases are +/// required. Closing drains all leases accepted before close was called. +final class MysqlDriver extends _MysqlDriver { + MysqlDriver._( + mysql.MySQLConnection connection, + MysqlOptions options, + String serverVersion, + ) : super(connection, options, serverVersion, SqlDialect.mysql); + + /// Opens MySQL 8.4 or newer and configures UTC and strict SQL behavior. + static Future open(MysqlOptions options) async { + final opened = await _openMysql(options, SqlDialect.mysql); + return MysqlDriver._(opened.connection, options, opened.version); + } +} + +/// Owns one MariaDB connection and serializes complete connection leases. +/// +/// The server must identify itself as MariaDB 10.6 or newer. MySQL servers are +/// rejected even when their protocol is compatible. +final class MariadbDriver extends _MysqlDriver { + MariadbDriver._( + mysql.MySQLConnection connection, + MariadbOptions options, + String serverVersion, + ) : super(connection, options, serverVersion, SqlDialect.mariadb); + + /// Opens MariaDB and configures UTC and strict SQL behavior. + static Future open(MariadbOptions options) async { + final opened = await _openMysql(options, SqlDialect.mariadb); + return MariadbDriver._(opened.connection, options, opened.version); + } +} + +abstract class _MysqlDriver implements Driver { + final mysql.MySQLConnection _connection; + final MysqlOptions _options; + final SqlDialect _dialect; + + /// Server version returned during the verified connection handshake. + final String serverVersion; + Future _tail = Future.value(); + Future? _closing; + bool _discarded = false; + + _MysqlDriver( + this._connection, + this._options, + this.serverVersion, + this._dialect, + ); + + @override + Capabilities get capabilities => Capabilities( + dialect: _dialect, + maxParameters: 65535, + returning: false, + streaming: false, + cancellation: false, + statementTimeout: true, + exactDecimal: true, + temporal: true, + ); + + /// Runs [action] exclusively and rolls back any transaction left open. + @override + Future run(Future Function(SqlConnection) action) { + if (_closing != null) { + return Future.error( + const OrmException( + 'DRIVER.CLOSED', + 'MySQL driver is closing or closed.', + ), + ); + } + final result = _tail.then((_) async { + if (_discarded || !_connection.connected) { + throw const OrmException( + 'DRIVER.CLOSED', + 'MySQL connection has been discarded. Open a new driver.', + ); + } + final lease = MysqlConnection(_connection, _options.queryTimeout); + try { + final result = await action(lease); + await lease.release(); + return result; + } catch (error, stack) { + try { + await lease.release(); + } catch (_) { + await lease.invalidate(); + } + Error.throwWithStackTrace(error, stack); + } finally { + if (lease.isInvalid) _discarded = true; + } + }); + _tail = result.then((_) {}, onError: (Object _, StackTrace _) {}); + return result; + } + + /// Rejects new leases, drains accepted work, and closes the connection. + @override + Future close() => _closing ??= _tail.then((_) async { + if (!_discarded && _connection.connected) { + try { + await _connection.close().timeout(_options.queryTimeout); + } catch (_) { + _connection.getSocket().destroy(); + rethrow; + } + } else { + _connection.getSocket().destroy(); + } + }); +} + +Future<({mysql.MySQLConnection connection, String version})> _openMysql( + MysqlOptions options, + SqlDialect dialect, +) async { + final url = options.url; + final schemes = dialect == SqlDialect.mysql + ? const {'mysql'} + : const {'mariadb', 'mysql'}; + if (!schemes.contains(url.scheme) || + url.host.isEmpty || + url.pathSegments.length != 1 || + url.pathSegments.single.isEmpty || + url.hasPort && (url.port < 1 || url.port > 65535) || + url.hasQuery || + url.hasFragment || + url.userInfo.isEmpty || + options.connectTimeout <= Duration.zero || + options.queryTimeout <= Duration.zero) { + throw ArgumentError( + 'Provide a ${dialect.name} URL with user and database, positive timeouts, ' + 'and typed options instead of URL query parameters.', + ); + } + final colon = url.userInfo.indexOf(':'); + final username = Uri.decodeComponent( + colon < 0 ? url.userInfo : url.userInfo.substring(0, colon), + ); + final password = colon < 0 + ? '' + : Uri.decodeComponent(url.userInfo.substring(colon + 1)); + if (username.isEmpty) throw ArgumentError('MySQL user must not be empty.'); + mysql.MySQLConnection? connection; + var expired = false; + try { + return await (() async { + final opened = await mysql.MySQLConnection.createConnection( + host: url.host, + port: url.hasPort ? url.port : 3306, + userName: username, + password: password, + databaseName: url.pathSegments.single, + secure: options.tls != MysqlTls.disable, + securityContext: options.securityContext, + // The upstream client's default accepts invalid certificates. + onBadCertificate: (_) => options.tls == MysqlTls.require, + ); + connection = opened; + if (expired) { + opened.getSocket().destroy(); + throw const OrmException('CONNECTION.TIMEOUT', 'Connection timed out.'); + } + await opened.connect( + timeoutMs: options.connectTimeout.inMilliseconds + .clamp(1, 2147483647) + .toInt(), + ); + final result = await opened.execute('SELECT VERSION()'); + final version = result.rows.single.colAt(0) as String; + final maria = version.toLowerCase().contains('mariadb'); + if (maria != (dialect == SqlDialect.mariadb)) { + throw OrmException( + 'DRIVER.ENGINE', + 'Expected ${dialect.name}, but the server reports $version.', + ); + } + final match = RegExp(r'^(\d+)\.(\d+)').firstMatch(version); + final major = match == null ? 0 : int.parse(match[1]!); + final minor = match == null ? 0 : int.parse(match[2]!); + if (maria + ? major < 10 || major == 10 && minor < 6 + : major < 8 || major == 8 && minor < 4) { + throw const OrmException( + 'DRIVER.VERSION', + 'MySQL 8.4+ or MariaDB 10.6+ is required.', + ); + } + // Do not silently coerce invalid dates, truncate values, or convert + // instants through the deployment host's time zone. + await opened.execute("SET SESSION time_zone = '+00:00'"); + await opened.execute( + "SET SESSION sql_mode = CONCAT_WS(',', NULLIF(@@SESSION.sql_mode, ''), " + "'ANSI_QUOTES', 'NO_BACKSLASH_ESCAPES', 'STRICT_ALL_TABLES', " + "'NO_ZERO_DATE', 'NO_ZERO_IN_DATE', 'ERROR_FOR_DIVISION_BY_ZERO', " + "'NO_ENGINE_SUBSTITUTION')", + ); + await opened.execute('SET SESSION autocommit = 1'); + return (connection: opened, version: version); + })().timeout(options.connectTimeout); + } on TimeoutException catch (error) { + expired = true; + connection?.getSocket().destroy(); + throw OrmException( + 'CONNECTION.TIMEOUT', + 'MySQL connection establishment timed out.', + cause: error, + ); + } catch (_) { + expired = true; + connection?.getSocket().destroy(); + rethrow; + } +} diff --git a/lib/src/mysql/failure.dart b/lib/src/mysql/failure.dart new file mode 100644 index 00000000..82c958e3 --- /dev/null +++ b/lib/src/mysql/failure.dart @@ -0,0 +1,24 @@ +import 'package:mysql_client_plus/exception.dart' as mysql; + +import '../../driver.dart'; + +/// A server-reported error. Numeric codes are preserved; SQLSTATE is not +/// exposed by mysql_client_plus and is not guessed from the error message. +final class MysqlFailure implements SqlFailure { + /// Original exception with the numeric code and server message. + final mysql.MySQLServerException cause; + + /// Wraps a server error without inferring SQLSTATE from its message. + const MysqlFailure(this.cause); + + /// Numeric MySQL or MariaDB server error code. + int get code => cause.errorCode; + @override + bool get retryTransaction => code == 1213 || code == 1205; + @override + bool get retryCommit => false; + @override + bool get commitRejected => code == 1213; + @override + String toString() => 'MysqlFailure($code): ${cause.message}'; +} diff --git a/lib/src/mysql/options.dart b/lib/src/mysql/options.dart new file mode 100644 index 00000000..9f130835 --- /dev/null +++ b/lib/src/mysql/options.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +/// Transport encryption and server identity checks for MySQL and MariaDB. +enum MysqlTls { + /// TLS with certificate and hostname verification. + verifyFull, + + /// TLS without certificate verification. Use only on a trusted network. + require, + + /// Unencrypted transport. The client requires TLS for SHA-2 authentication. + disable, +} + +/// Configuration for one owned MySQL connection. +/// +/// The driver queues complete connection leases and configures UTC, strict SQL +/// modes, and autocommit. Additional drivers provide independent concurrency. +class MysqlOptions { + /// A database URL with one database path segment and no query or fragment. + final Uri url; + + /// Transport policy; certificate verification is enabled by default. + final MysqlTls tls; + + /// Deadline for opening and configuring the connection. + final Duration connectTimeout; + + /// Default statement deadline, overridable for each execution. + /// + /// Expiry discards the connection; the server-side outcome may be unknown. + final Duration queryTimeout; + + /// Optional trust roots or client credentials for TLS. + final SecurityContext? securityContext; + + /// Creates connection configuration; the driver validates it when opened. + const MysqlOptions({ + required this.url, + this.tls = MysqlTls.verifyFull, + this.connectTimeout = const Duration(seconds: 10), + this.queryTimeout = const Duration(seconds: 30), + this.securityContext, + }); +} + +/// MariaDB configuration with engine identity checked during connection setup. +final class MariadbOptions extends MysqlOptions { + /// Creates options for an owned MariaDB connection. + const MariadbOptions({ + required super.url, + super.tls, + super.connectTimeout, + super.queryTimeout, + super.securityContext, + }); +} diff --git a/lib/src/mysql/values.dart b/lib/src/mysql/values.dart index c0884f33..dfc8d278 100644 --- a/lib/src/mysql/values.dart +++ b/lib/src/mysql/values.dart @@ -1,16 +1,21 @@ -part of '../../drivers/mysql.dart'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:mysql_client_plus/mysql_protocol.dart' as protocol; + +import '../../values.dart'; final _mysqlMinInt = BigInt.from(-9223372036854775808); final _mysqlMaxInt = BigInt.from(9223372036854775807); -int _checkedMysqlInt(BigInt value) { +int checkedMysqlInt(BigInt value) { if (value < _mysqlMinInt || value > _mysqlMaxInt) { throw const FormatException('MySQL integer exceeds Dart signed int64.'); } return value.toInt(); } -Object? _mysqlParameter(Object? value) => switch (value) { +Object? mysqlParameter(Object? value) => switch (value) { null || String() || int() || bool() || Uint8List() => value, double() => _finiteMysqlReal(value), SqlReal() => _finiteMysqlReal(value.value), @@ -61,7 +66,7 @@ String _mysqlDateTime(DateTime value) { return '$date $time'; } -Object? _mysqlValue(Object? value, int type, {required bool binary}) { +Object? mysqlValue(Object? value, int type, {required bool binary}) { if (value == null) return null; switch (type) { case protocol.mysqlColumnTypeString: @@ -77,7 +82,7 @@ Object? _mysqlValue(Object? value, int type, {required bool binary}) { case protocol.mysqlColumnTypeLongLong: case protocol.mysqlColumnTypeInt24: case protocol.mysqlColumnTypeYear: - return _checkedMysqlInt(BigInt.parse(value as String)); + return checkedMysqlInt(BigInt.parse(value as String)); case protocol.mysqlColumnTypeFloat: case protocol.mysqlColumnTypeDouble: return double.parse(value as String); diff --git a/lib/src/observation.dart b/lib/src/observation.dart deleted file mode 100644 index c98c2660..00000000 --- a/lib/src/observation.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of '../orm.dart'; - -/// Synchronous ORM decoding/mapping and associated row grouping. Excludes SQL, -/// acquisition, compilation, waiting for the consumer and other client work. -final class DecodeEvent { - /// Null for a batch RETURNING result assembled from multiple commands. - final String? sql; - final int inputRows; - final Duration elapsed; - final Object? error; - const DecodeEvent({ - required this.sql, - required this.inputRows, - required this.elapsed, - this.error, - }); -} - -extension _DatabaseObservation on Database { - T _observeDecode(String? sql, int inputRows, T Function() action) { - final observer = onDecode; - if (observer == null) return action(); - final clock = Stopwatch()..start(); - Object? error; - try { - return action(); - } catch (e) { - error = e; - rethrow; - } finally { - clock.stop(); - try { - observer( - DecodeEvent( - sql: sql, - inputRows: inputRows, - elapsed: clock.elapsed, - error: error, - ), - ); - } catch (_) {} // Observers cannot change a result or its failure. - } - } -} diff --git a/lib/src/orm/changes.dart b/lib/src/orm/changes.dart new file mode 100644 index 00000000..7f13958e --- /dev/null +++ b/lib/src/orm/changes.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:meta/meta.dart' show internal; + +import '../../driver.dart'; +import '../../schema_model.dart'; + +final _hubs = Expando(); + +/// @nodoc +@internal +ChangeHub changesFor(Driver driver) => _hubs[driver] ??= ChangeHub(); + +/// @nodoc +@internal +abstract interface class ChangeSubscription { + Set get tables; + void invalidate(); + Future stop(); +} + +/// @nodoc +@internal +void mergeChanges(Map target, Map source) { + for (final entry in source.entries) { + target[entry.key] = (target[entry.key] ?? false) || entry.value; + } +} + +/// Shared by views of the same driver, without global result caching. +/// @nodoc +@internal +final class ChangeHub { + final _schemas = Expando(); + final Map _tables = {}; + final Map> _deleteEffects = {}; + final Set watches = {}; + bool closed = false; + + void register(Iterable schema) { + if (!closed && _schemas[schema] != true) { + _schemas[schema] = true; + for (final table in schema) { + registerTable(table); + } + } + } + + void registerTable(TableSchema table) { + if (closed || identical(_tables[table.name], table)) return; + final previous = _tables[table.name]; + if (previous != null) { + for (final fk in previous.foreignKeys) { + final effects = _deleteEffects[fk.target]; + effects?.remove(table.name); + if (effects?.isEmpty ?? false) _deleteEffects.remove(fk.target); + } + } + _tables[table.name] = table; + for (final fk in table.foreignKeys) { + if (!{'CASCADE', 'SET NULL', 'SET DEFAULT'}.contains(fk.onDelete)) { + continue; + } + final effects = _deleteEffects.putIfAbsent(fk.target, () => {}); + effects[table.name] = + (effects[table.name] ?? false) || fk.onDelete == 'CASCADE'; + } + } + + void publish(Map changes) { + if (closed || changes.isEmpty || watches.isEmpty) return; + final affected = changes.keys.toSet(); + final deletes = [ + for (final e in changes.entries) + if (e.value) e.key, + ]; + final visited = {}; + while (deletes.isNotEmpty) { + final table = deletes.removeLast(); + if (!visited.add(table)) continue; + for (final effect + in (_deleteEffects[table] ?? const {}).entries) { + affected.add(effect.key); + if (effect.value) deletes.add(effect.key); + } + } + for (final watch in watches.toList()) { + if (watch.tables.any(affected.contains)) watch.invalidate(); + } + } + + Future stop() async { + closed = true; + await Future.wait(watches.toList().map((watch) => watch.stop())); + _tables.clear(); + _deleteEffects.clear(); + } +} diff --git a/lib/src/database.dart b/lib/src/orm/database.dart similarity index 57% rename from lib/src/database.dart rename to lib/src/orm/database.dart index 7d72d341..175c041e 100644 --- a/lib/src/database.dart +++ b/lib/src/orm/database.dart @@ -1,14 +1,30 @@ -part of '../orm.dart'; +import 'package:meta/meta.dart' show internal; -/// Typed ORM access over a raw SQL runtime. The runtime owns connections, -/// transactions and cursor lifetimes; this layer adds tables and invalidation. +import '../../runtime.dart'; +import '../../sql.dart'; +import 'changes.dart'; +import 'observation.dart'; +import 'observation.dart' as observation; + +/// Typed queries and change notifications over an owned SQL runtime. +/// +/// [sql] manages connections, transactions, and cursor lifetimes. Generated table +/// access and [table] add typed queries; [invalidate] reports external changes to +/// watches. Session and transaction views share ownership with their root runtime +/// and expire when their callback ends. +/// +/// {@category Databases} class Database extends QueryContext { + /// Raw SQL runtime shared by this database and its borrowed session views. final SqlDatabase sql; + + /// Optional decode observer; observer exceptions cannot change query outcomes. final void Function(DecodeEvent)? onDecode; - late final _ChangeHub _changes = _changeHubs[driver] ??= _ChangeHub(); + late final ChangeHub _changes = changesFor(driver); final Map _pendingChanges = {}; bool _invalidationQueued = false; + /// Takes ownership of [driver] through a new SQL runtime. Database( Driver driver, { void Function(QueryEvent)? onQuery, @@ -19,6 +35,10 @@ class Database extends QueryContext { onDecode: onDecode, ); + /// Adds typed queries and watches to an existing SQL runtime. + /// + /// This shares [sql]'s lifecycle: closing a root wrapper closes that runtime; + /// wrapping a borrowed session does not grant ownership of its driver. Database.fromSql(this.sql, {this.onDecode}) { if (!sql.inSession) { final removeChanges = sql.addChangeListener( @@ -31,29 +51,47 @@ class Database extends QueryContext { } } + /// Driver owned by the root SQL runtime. Driver get driver => sql.driver; + + /// Operations supported by the concrete engine, adapter, and platform. @override Capabilities get capabilities => sql.capabilities; + + /// Connected database engine used for SQL rendering and validation. @override SqlDialect get dialect => sql.dialect; + + /// Whether this view belongs to a transaction or savepoint callback. @override bool get inTransaction => sql.inTransaction; + + /// Whether this view already holds a connection lease. @override bool get inSession => sql.inSession; + + /// Statement and cursor observer inherited from the SQL runtime. void Function(QueryEvent)? get onQuery => sql.onQuery; + + /// Connection acquisition observer inherited from the SQL runtime. void Function(AcquisitionEvent)? get onAcquire => sql.onAcquire; + /// Binds a manually defined typed table to this database or session. @override TableSet table(Table definition) => TableSet(this, definition); - /// Register declared FK effects for query invalidation. + /// Registers physical schema for foreign-key-aware query invalidation. + /// + /// Generated clients call this when binding tables. Registration executes no + /// DDL and performs no database introspection. @override void registerSchema(Iterable tables) => _changes.register(tables); /// Explicit notification for raw SQL, triggers or externally committed work. - /// A transaction defers notification until commit; rollback discards it. + /// A transaction defers notification until commit; confirmed rollback discards + /// it. An uncertain commit invalidates conservatively. void invalidate(Iterable tables) { checkActive(); _recordChanges(tables, cascade: true); @@ -69,17 +107,26 @@ class Database extends QueryContext { _changes.publish(changes); return; } - _mergeChanges(_pendingChanges, changes); + mergeChanges(_pendingChanges, changes); if (!_invalidationQueued) { _invalidationQueued = true; sql.deferInvalidation(() => _changes.publish(_pendingChanges)); } } + /// @nodoc + @internal void checkActive() => sql.checkActive(); + + /// @nodoc + @internal @override void markFailed() => sql.markFailed(); + /// Runs an advanced raw operation using an acquired or existing connection. + /// + /// The connection remains owned by [sql] and must not escape [action]. This + /// does not start a transaction or infer which physical tables were changed. @override Future run( Future Function(SqlConnection) action, { @@ -91,6 +138,8 @@ class Database extends QueryContext { acquisitionTimeoutError: acquisitionTimeoutError, ); + /// @nodoc + @internal @override Future executeOn( SqlConnection connection, @@ -98,6 +147,10 @@ class Database extends QueryContext { ExecutionOptions options = const ExecutionOptions(), }) => sql.executeOn(connection, command, options: options); + /// Executes bound SQL and returns raw rows without typed model decoding. + /// + /// Declare [changedTables] for writes that should invalidate watched queries. + /// Notifications follow transaction commit and rollback boundaries. @override Future execute( SqlCommand command, { @@ -105,6 +158,8 @@ class Database extends QueryContext { Iterable changedTables = const [], }) => executeCommand(command, options: options, changedTables: changedTables); + /// @nodoc + @internal @override Future executeCommand( SqlCommand command, { @@ -137,6 +192,10 @@ class Database extends QueryContext { }, acquire: options.acquisition); } + /// Holds one connection for [action] without automatically starting a transaction. + /// + /// The supplied view expires when the callback finishes. Await all its work + /// and finish streams before returning; nested leased sessions are rejected. Future session( Future Function(Database session) action, { AcquisitionOptions acquire = const AcquisitionOptions(), @@ -145,6 +204,16 @@ class Database extends QueryContext { acquire: acquire, ); + /// Runs typed queries on one transaction and returns after confirmed commit. + /// + /// Use the supplied transaction view for every operation. A failed statement + /// prevents commit even when its error is caught; use [savepoint] for a + /// recoverable sub-operation. Confirmed rollback drops pending notifications; + /// an uncertain commit invalidates watches conservatively. + /// + /// [retry] explicitly permits replay after confirmed rollback, so the callback + /// must be safe to repeat. An uncertain commit is never automatically replayed. + /// [timeout] applies after acquisition; retry budgets also include acquisition. Future transaction( Future Function(Database tx) action, { TransactionOptions? options, @@ -161,9 +230,15 @@ class Database extends QueryContext { retry: retry, ); + /// Runs recoverable nested work inside an existing transaction. + /// + /// Use the child view exclusively until it finishes. On failure, the runtime + /// rolls back to its savepoint; failed cleanup invalidates the connection. Future savepoint(Future Function(Database tx) action) => sql.savepoint((tx) => action(Database.fromSql(tx, onDecode: onDecode))); + /// @nodoc + @internal @override Stream streamRows( SqlCommand command, { @@ -182,16 +257,28 @@ class Database extends QueryContext { decode: decode, ); + /// Permanently invalidates this view's leased connection. + /// + /// Requires a session or transaction view. Discarding is for unrecoverable + /// state; it does not establish the outcome of a write already submitted. Future discard() => sql.discard(); + + /// Stops watches and streams, drains pending work, and closes the root runtime. + /// + /// Borrowed session and transaction views cannot close their owner's driver. Future close() => sql.close(); + /// @nodoc + @internal @override Future atomic( Future Function(QueryContext) action, { AcquisitionOptions acquire = const AcquisitionOptions(), }) => transaction(action, acquire: acquire); + /// @nodoc + @internal @override T observeDecode(String? sql, int inputRows, T Function() action) => - _observeDecode(sql, inputRows, action); + observation.observeDecode(onDecode, sql, inputRows, action); } diff --git a/lib/src/orm/observation.dart b/lib/src/orm/observation.dart new file mode 100644 index 00000000..eabcf273 --- /dev/null +++ b/lib/src/orm/observation.dart @@ -0,0 +1,58 @@ +import 'package:meta/meta.dart' show internal; + +/// Synchronous ORM decoding/mapping and associated row grouping. Excludes SQL, +/// acquisition, compilation, waiting for the consumer and other client work. +/// +/// {@category Observability} +final class DecodeEvent { + /// Null for a batch RETURNING result assembled from multiple commands. + final String? sql; + + /// Number of raw rows passed into the measured decoding operation. + final int inputRows; + + /// Synchronous decode, mapping, and grouping time. + final Duration elapsed; + + /// Decoder or mapper failure, or null when decoding completed. + final Object? error; + + /// Captures decode cost without retaining decoded rows or bound parameters. + const DecodeEvent({ + required this.sql, + required this.inputRows, + required this.elapsed, + this.error, + }); +} + +/// @nodoc +@internal +T observeDecode( + void Function(DecodeEvent)? observer, + String? sql, + int inputRows, + T Function() action, +) { + if (observer == null) return action(); + final clock = Stopwatch()..start(); + Object? error; + try { + return action(); + } catch (e) { + error = e; + rethrow; + } finally { + clock.stop(); + try { + observer( + DecodeEvent( + sql: sql, + inputRows: inputRows, + elapsed: clock.elapsed, + error: error, + ), + ); + } catch (_) {} // Observers cannot change a result or its failure. + } +} diff --git a/lib/src/watch.dart b/lib/src/orm/watch.dart similarity index 62% rename from lib/src/watch.dart rename to lib/src/orm/watch.dart index 0e9cd356..c0d7fe37 100644 --- a/lib/src/watch.dart +++ b/lib/src/orm/watch.dart @@ -1,95 +1,32 @@ -part of '../orm.dart'; +import 'dart:async'; -final _changeHubs = Expando<_ChangeHub>(); - -void _mergeChanges(Map target, Map source) { - for (final entry in source.entries) { - target[entry.key] = (target[entry.key] ?? false) || entry.value; - } -} - -/// Shared by views of the same driver, without global result caching. -final class _ChangeHub { - final _schemas = Expando(); - final Map _tables = {}; - final Map> _deleteEffects = {}; - final Set<_QueryWatch> watches = {}; - bool closed = false; - - void register(Iterable schema) { - if (!closed && _schemas[schema] != true) { - _schemas[schema] = true; - for (final table in schema) { - registerTable(table); - } - } - } - - void registerTable(TableSchema table) { - if (closed || identical(_tables[table.name], table)) return; - final previous = _tables[table.name]; - if (previous != null) { - for (final fk in previous.foreignKeys) { - final effects = _deleteEffects[fk.target]; - effects?.remove(table.name); - if (effects?.isEmpty ?? false) _deleteEffects.remove(fk.target); - } - } - _tables[table.name] = table; - for (final fk in table.foreignKeys) { - if (!{'CASCADE', 'SET NULL', 'SET DEFAULT'}.contains(fk.onDelete)) { - continue; - } - final effects = _deleteEffects.putIfAbsent(fk.target, () => {}); - effects[table.name] = - (effects[table.name] ?? false) || fk.onDelete == 'CASCADE'; - } - } - - void publish(Map changes) { - if (closed || changes.isEmpty || watches.isEmpty) return; - final affected = changes.keys.toSet(); - final deletes = [ - for (final e in changes.entries) - if (e.value) e.key, - ]; - final visited = {}; - while (deletes.isNotEmpty) { - final table = deletes.removeLast(); - if (!visited.add(table)) continue; - for (final effect - in (_deleteEffects[table] ?? const {}).entries) { - affected.add(effect.key); - if (effect.value) deletes.add(effect.key); - } - } - for (final watch in watches.toList()) { - if (watch.tables.any(affected.contains)) watch.invalidate(); - } - } - - Future stop() async { - closed = true; - await Future.wait(watches.toList().map((watch) => watch.stop())); - _tables.clear(); - _deleteEffects.clear(); - } -} +import '../../sql.dart'; +import 'database.dart'; +import 'changes.dart'; +/// Re-executes a root database query after relevant committed changes. +/// +/// {@category Observability} extension WatchQuery on Query { /// Initial snapshot followed by fresh snapshots after relevant committed writes. /// Add tables hidden inside raw SQL with [reads]. Read errors are emitted without /// closing the stream, allowing a later invalidation to retry the query. + /// + /// Watches require a root database outside leased sessions and transactions. + /// ORM writes report their physical tables automatically; raw or external + /// writes require explicit invalidation. Cancelling the subscription releases + /// its listener, and closing the database stops its watches. Stream> watch({ Iterable reads = const [], ExecutionOptions options = const ExecutionOptions(), }) => _QueryWatch(this, List.unmodifiable(reads), options).controller.stream; } -final class _QueryWatch { +final class _QueryWatch implements ChangeSubscription { final Query query; final List extraReads; final ExecutionOptions options; + @override final Set tables = {}; late final controller = StreamController>( onListen: _start, @@ -124,7 +61,7 @@ final class _QueryWatch { 'Watch a root database query outside leased sessions and transactions.', ); } - if (db._changes.closed) { + if (changesFor(db.driver).closed) { throw const OrmException('SESSION.CLOSED', 'Database is closed.'); } final reads = query.dependencies; @@ -136,10 +73,10 @@ final class _QueryWatch { } for (final table in [...reads.tables, ...extraReads]) { tables.add(table.name); - db._changes.registerTable(table); + changesFor(db.driver).registerTable(table); } // Subscribe before the initial read so concurrent commits cannot be lost. - db._changes.watches.add(this); + changesFor(db.driver).watches.add(this); _removeCancellation = options.cancellation?.listen(() { unawaited( stop( @@ -156,6 +93,7 @@ final class _QueryWatch { } } + @override void invalidate() { _dirty = true; _schedule(); @@ -205,13 +143,14 @@ final class _QueryWatch { } } + @override Future stop({Object? error, StackTrace? stack}) { if (_stopping case final stopping?) return stopping; final done = Completer(); _stopping = done.future; _stopped = true; if (query.database case final Database db) { - db._changes.watches.remove(this); + changesFor(db.driver).watches.remove(this); } _removeCancellation?.call(); _readCancellation?.cancel(); diff --git a/lib/src/postgres/connection.dart b/lib/src/postgres/connection.dart new file mode 100644 index 00000000..1e5293d5 --- /dev/null +++ b/lib/src/postgres/connection.dart @@ -0,0 +1,240 @@ +import 'dart:async'; + +import 'package:postgres/postgres.dart' as pg; + +import '../../driver.dart'; +import 'failure.dart'; + +final class PostgresConnection implements SqlConnection { + // package:postgres does not expose ReadyForQuery transaction status publicly. + @override + bool? get transactionActive => null; + final pg.Connection connection; + final Future Function()? _control; + final Expando _backendIds; + final Duration? _queryTimeout; + bool _busy = false; + int _cursorId = 0; + PostgresConnection( + this.connection, + this._control, + this._backendIds, + this._queryTimeout, + ); + + // ResultStream is buffered only for this bounded statement. Its subscription + // has no hidden timeout task that could cancel a later statement. We own and + // await cancellation below, including connection-control cleanup. + Future _query(SqlCommand command) async { + final statement = await connection.prepare( + pg.Sql( + command.sql, + types: List.filled(command.parameters.length, pg.Type.unspecified), + ), + ); + try { + final rows = []; + final subscription = statement + .bind([ + for (final p in command.parameters) p is SqlReal ? p.value : p, + ]) + .listen(rows.add); + try { + await subscription.asFuture(); + final schema = await subscription.schema; + final jsonColumns = { + for (final (index, column) in schema.columns.indexed) + if (column.typeOid == pg.Type.json.oid || + column.typeOid == pg.Type.jsonb.oid) + index, + }; + // The driver parses JSON strings and JSON null into ordinary Dart values. + // Retain their distinction from SQL text and SQL NULL without reserializing. + final List> values = jsonColumns.isEmpty + ? rows + : [ + for (final row in rows) + [ + for (var i = 0; i < row.length; i++) + jsonColumns.contains(i) && !row.isSqlNull(i) + ? SqlJson(row[i]) + : row[i], + ], + ]; + return SqlResult( + values, + columns: [for (final c in schema.columns) c.columnName ?? ''], + affectedRows: await subscription.affectedRows, + ); + } finally { + await subscription.cancel(); + } + } finally { + await statement.dispose(); + } + } + + @override + Future execute( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }) async { + options.check(); + if (_busy) { + throw const OrmException( + 'SESSION.BUSY', + 'Await the active statement before using this connection again.', + ); + } + final timeout = options.timeout ?? _queryTimeout; + if (timeout != null && timeout <= Duration.zero) { + throw ArgumentError.value(timeout, 'timeout'); + } + if ((options.cancellation != null || timeout != null) && _control == null) { + throw const OrmException( + 'CAPABILITY.CANCEL', + 'A borrowed PostgreSQL pool needs a cancellation connection factory.', + ); + } + _busy = true; + Future? cancelling; + var completed = false, timedOut = false, statementStarted = false; + Timer? deadline; + void Function()? unsubscribe; + try { + if (options.cancellation != null || timeout != null) { + var backendId = _backendIds[connection.info]; + void cancel() { + if (completed || cancelling != null) return; + final pid = backendId; + cancelling = () async { + // PID discovery is part of the same deadline. Until it completes, + // discard this connection; the application SQL has not been sent. + if (pid == null) { + await connection.close(force: true); + return; + } + try { + final control = await _control!(); + try { + if (!completed) { + final result = await control.execute( + pg.Sql( + r'SELECT pg_cancel_backend($1)', + types: [pg.Type.integer], + ), + parameters: [pid], + ); + if (result.single.single != true && !completed) { + await connection.close(force: true); + } + } + } finally { + await control.close(force: true); + } + } catch (_) { + await connection.close(force: true); + rethrow; + } + }(); + // Observe failures immediately; the operation awaits cleanup below. + unawaited(cancelling!.catchError((Object _) {})); + } + + unsubscribe = options.cancellation?.listen(cancel); + if (timeout != null) { + deadline = Timer(timeout, () { + timedOut = true; + cancel(); + }); + } + options.check(); + backendId ??= _backendIds[connection.info] = + (await _query(SqlCommand('SELECT pg_backend_pid()'))) + .rows + .single + .single + as int; + options.check(); + if (timedOut) { + throw const OrmException( + 'OPERATION.TIMEOUT', + 'PostgreSQL PID discovery exceeded the statement deadline.', + ); + } + } + statementStarted = true; + return await _query(command); + } catch (error) { + if ((timedOut || options.cancellation?.isCancelled == true) && + (!statementStarted || + error is pg.ServerException && error.code == '57014')) { + throw OrmException( + timedOut ? 'OPERATION.TIMEOUT' : 'OPERATION.CANCELLED', + statementStarted ? 'PostgreSQL stopped the statement.' : 'PostgreSQL PID discovery was interrupted before the statement.', + cause: error, + ); + } + if (error is pg.ServerException) throw PostgresFailure(error); + rethrow; + } finally { + completed = true; + deadline?.cancel(); + unsubscribe?.call(); + try { + await cancelling; + } finally { + _busy = false; + } + } + } + + @override + Future openCursor( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }) async { + final name = 'orm_cursor_${_cursorId++}'; + await execute( + SqlCommand( + 'DECLARE "$name" NO SCROLL CURSOR FOR ${command.sql}', + command.parameters, + ), + options: options, + ); + return _PostgresCursor(this, name); + } + + @override + Future invalidate() => connection.close(force: true); +} + +final class _PostgresCursor( + final PostgresConnection connection, + final String name, +) implements SqlCursor { + bool _closed = false; + @override + Future fetch( + int count, { + ExecutionOptions options = const ExecutionOptions(), + }) { + if (_closed) throw const OrmException('CURSOR.CLOSED', 'Cursor has ended.'); + if (count < 1) throw ArgumentError.value(count, 'count'); + return connection.execute( + SqlCommand('FETCH FORWARD $count FROM "$name"'), + options: options, + ); + } + + @override + Future close() async { + if (_closed) return; + _closed = true; + try { + await connection.execute(SqlCommand('CLOSE "$name"')); + } on PostgresFailure catch (e) { + if (e.code != '25P02' && e.code != '34000') rethrow; + } + } +} diff --git a/lib/src/postgres/driver.dart b/lib/src/postgres/driver.dart new file mode 100644 index 00000000..9849b3e5 --- /dev/null +++ b/lib/src/postgres/driver.dart @@ -0,0 +1,189 @@ +import 'dart:async'; + +import 'package:postgres/postgres.dart' as pg; + +import '../../driver.dart'; +import 'connection.dart'; +import 'options.dart'; +import 'temporal.dart'; + +/// Owns or borrows a PostgreSQL pool and leases one connection per callback. +/// +/// Owned pools configure exact temporal codecs and cancellation connections. +/// A borrowed pool enables these capabilities only when explicitly configured. +final class PostgresDriver implements Driver { + final pg.Pool _pool; + final bool _ownsPool; + final bool _temporal; + final Future Function()? _cancelConnection; + final Expando _backendIds = Expando(); + final Duration? _queryTimeout; + final Duration? _connectTimeout; + bool _closed = false; + + /// Creates an owned pool; physical connections open as leases are acquired. + PostgresDriver(PostgresOptions options) + : _pool = _createPool(options), + _ownsPool = true, + _temporal = true, + _queryTimeout = options.queryTimeout, + _connectTimeout = options.connectTimeout, + _cancelConnection = (() => _openControl(options)); + + /// Uses a caller-owned pool, which remains open when this driver closes. + /// + /// Supply [cancellationConnection] to support statement cancellation. Enable + /// `temporal` only if the pool was created with [postgresTypeRegistry]. + PostgresDriver.borrow( + pg.Pool pool, { + Future Function()? cancellationConnection, + this._queryTimeout, + // Enable only when the pool uses postgresTypeRegistry(). + this._temporal = false, + }) : _pool = pool, + _ownsPool = false, + _connectTimeout = null, + _cancelConnection = cancellationConnection; + + static Future _openControl(PostgresOptions options) { + final url = options.url, colon = options.url.userInfo.indexOf(':'); + return pg.Connection.open( + pg.Endpoint( + host: url.host, + port: url.hasPort ? url.port : 5432, + database: url.pathSegments.single, + username: url.userInfo.isEmpty + ? null + : Uri.decodeComponent( + colon < 0 ? url.userInfo : url.userInfo.substring(0, colon), + ), + password: colon < 0 + ? null + : Uri.decodeComponent(url.userInfo.substring(colon + 1)), + ), + settings: pg.ConnectionSettings( + connectTimeout: options.connectTimeout, + queryTimeout: const Duration(seconds: 5), + sslMode: switch (options.tls) { + PostgresTls.verifyFull => pg.SslMode.verifyFull, + PostgresTls.require => pg.SslMode.require, + PostgresTls.disable => pg.SslMode.disable, + }, + applicationName: '${options.applicationName}/cancel', + ), + ); + } + + static pg.Pool _createPool(PostgresOptions options) { + final url = options.url; + if (!{'postgres', 'postgresql'}.contains(url.scheme) || + url.host.isEmpty || + url.pathSegments.length != 1 || + options.maxConnections < 1) { + throw ArgumentError('Provide a PostgreSQL URL and a positive pool size.'); + } + if (options.queryTimeout <= Duration.zero || + options.poolTimeout <= Duration.zero || + options.connectTimeout <= Duration.zero) { + throw ArgumentError('PostgreSQL timeouts must be positive.'); + } + if (url.hasQuery || url.hasFragment) { + throw ArgumentError( + 'Use typed PostgresOptions instead of URL query options.', + ); + } + final colon = url.userInfo.indexOf(':'); + final user = colon < 0 ? url.userInfo : url.userInfo.substring(0, colon); + final password = colon < 0 + ? null + : Uri.decodeComponent(url.userInfo.substring(colon + 1)); + return pg.Pool.withEndpoints( + [ + pg.Endpoint( + host: url.host, + port: url.hasPort ? url.port : 5432, + database: url.pathSegments.single, + username: user.isEmpty ? null : Uri.decodeComponent(user), + password: password, + ), + ], + settings: pg.PoolSettings( + maxConnectionCount: options.maxConnections, + connectTimeout: options.poolTimeout, + queryTimeout: null, + applicationName: options.applicationName, + timeZone: 'UTC', + typeRegistry: postgresTypeRegistry(), + onOpen: options.schema == null + ? null + : (connection) async { + final schema = '"${options.schema!.replaceAll('"', '""')}"'; + await connection.execute( + pg.Sql( + r"SELECT pg_catalog.set_config('search_path', $1, false)", + types: [pg.Type.text], + ), + parameters: [schema], + ); + }, + sslMode: switch (options.tls) { + PostgresTls.verifyFull => pg.SslMode.verifyFull, + PostgresTls.require => pg.SslMode.require, + PostgresTls.disable => pg.SslMode.disable, + }, + ), + ); + } + + @override + Capabilities get capabilities => Capabilities( + dialect: SqlDialect.postgres, + maxParameters: 65535, + streaming: true, + cancellation: _cancelConnection != null, + exactDecimal: true, + temporal: _temporal, + ); + + /// Leases a pooled connection for the lifetime of the callback future. + @override + Future run(Future Function(SqlConnection) action) async { + if (_closed) { + throw const OrmException('DRIVER.CLOSED', 'PostgreSQL driver is closed.'); + } + var acquired = false; + try { + return await _pool.withConnection( + (connection) { + acquired = true; + return action( + PostgresConnection( + connection, + _cancelConnection, + _backendIds, + _queryTimeout, + ), + ); + }, + settings: _connectTimeout == null + ? null + : pg.ConnectionSettings(connectTimeout: _connectTimeout), + ); + } on TimeoutException catch (error) { + if (acquired) rethrow; + throw OrmException( + 'CONNECTION.TIMEOUT', + 'PostgreSQL connection acquisition timed out.', + cause: error, + ); + } + } + + /// Rejects new leases and closes the pool only when this driver owns it. + @override + Future close() async { + if (_closed) return; + _closed = true; + if (_ownsPool) await _pool.close(); + } +} diff --git a/lib/src/postgres/failure.dart b/lib/src/postgres/failure.dart new file mode 100644 index 00000000..00a65772 --- /dev/null +++ b/lib/src/postgres/failure.dart @@ -0,0 +1,23 @@ +import 'package:postgres/postgres.dart' as pg; + +import '../../driver.dart'; + +/// A server-reported error, preserving SQLSTATE and the original exception. +final class PostgresFailure implements SqlFailure { + /// The original driver exception, including server diagnostics. + final pg.ServerException cause; + + /// Wraps a PostgreSQL server failure without losing its SQLSTATE. + const PostgresFailure(this.cause); + + /// Five-character SQLSTATE reported by the server, when available. + String? get code => cause.code; + @override + bool get retryTransaction => code == '40001' || code == '40P01'; + @override + bool get retryCommit => false; + @override + bool get commitRejected => code?.startsWith('23') == true || retryTransaction; + @override + String toString() => cause.toString(); +} diff --git a/lib/src/postgres/options.dart b/lib/src/postgres/options.dart new file mode 100644 index 00000000..27d156f2 --- /dev/null +++ b/lib/src/postgres/options.dart @@ -0,0 +1,53 @@ +/// Transport encryption and server identity checks for PostgreSQL connections. +enum PostgresTls { + /// Encrypts traffic and verifies the server certificate and hostname. + verifyFull, + + /// Encrypts traffic without verifying the server identity. + require, + + /// Opens an unencrypted connection. + disable, +} + +/// Configuration for an owned PostgreSQL connection pool. +/// +/// Connection details belong in [url]; timeouts, TLS, and pool size use the typed +/// options here. URL query parameters and fragments are rejected when opening. +final class PostgresOptions { + /// A `postgres://` or `postgresql://` URL with one database path segment. + final Uri url; + + /// Transport policy; server identity verification is enabled by default. + final PostgresTls tls; + + /// Maximum simultaneous physical connections; must be at least one. + final int maxConnections; + + /// Deadline for establishing each new physical connection. + final Duration connectTimeout; + + /// Driver pool queue limit; separate from opening a new connection. + final Duration poolTimeout; + + /// Default statement deadline, overridable for each execution. + final Duration queryTimeout; + + /// Label exposed by PostgreSQL in connection and activity diagnostics. + final String applicationName; + + /// Optional, quoted schema used as the connection search path. + final String? schema; + + /// Creates pool configuration; opening the driver validates its values. + const PostgresOptions({ + required this.url, + this.tls = PostgresTls.verifyFull, + this.maxConnections = 8, + this.connectTimeout = const Duration(seconds: 10), + this.poolTimeout = const Duration(seconds: 30), + this.queryTimeout = const Duration(seconds: 30), + this.applicationName = 'dart-orm', + this.schema, + }); +} diff --git a/lib/src/postgres/temporal.dart b/lib/src/postgres/temporal.dart index edf0e386..fafa89e5 100644 --- a/lib/src/postgres/temporal.dart +++ b/lib/src/postgres/temporal.dart @@ -1,4 +1,8 @@ -part of '../../drivers/postgres.dart'; +import 'dart:typed_data'; + +import 'package:postgres/postgres.dart' as pg; + +import '../../values.dart'; /// Native calendar values and UTC instants, without integer overflow. /// Local calendar types cover PostgreSQL's full finite range; UTC instants diff --git a/lib/src/batch.dart b/lib/src/query/batch.dart similarity index 54% rename from lib/src/batch.dart rename to lib/src/query/batch.dart index e97a5dc1..364fa483 100644 --- a/lib/src/batch.dart +++ b/lib/src/query/batch.dart @@ -1,65 +1,92 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import 'context.dart'; +import 'mutation.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; /// Inserts consecutive rows of the same shape together, splitting at the /// driver's parameter limit. All chunks share one transaction by default. +/// +/// Preparing or compiling a batch performs no I/O. Inside an existing +/// transaction, every chunk uses that transaction. A failure between chunks +/// marks it failed, preventing a caught error from committing a partial batch. final class BatchInsert { + /// Query context that owns the batch's execution and transaction scope. final QueryContext database; - final F _fields; - final _QueryState _state; - final List> _rows; - BatchInsert._( + + /// @nodoc + @internal + final F queryFields; + + /// @nodoc + @internal + final QueryState queryState; + + /// @nodoc + @internal + final List> insertRows; + + /// @nodoc + @internal + BatchInsert.internal( this.database, - this._fields, - this._state, + this.queryFields, + this.queryState, List> rows, - ) : _rows = List.unmodifiable(rows); + ) : insertRows = List.unmodifiable(rows); - List _compile([_SelectionPlan? selection]) { + /// @nodoc + @internal + List compileQuery([SelectionPlan? selection]) { final commands = []; var chunk = >[]; List? shape; var parameters = 0; - final extra = _Writer( + final extra = SqlWriter( database.dialect, - {_state.source: 't0'}, + {queryState.source: 't0'}, database: database, exactDecimal: database.capabilities.exactDecimal, temporal: database.capabilities.temporal, ); if (selection != null) { for (final column in selection.columns) { - column._node.write(extra); + column.expressionNode.write(extra); } } final limit = database.capabilities.maxParameters - extra.parameters.length; void flush() { if (chunk.isEmpty) return; commands.add( - Mutation._( + Mutation.internal( database, - _fields, - _state, - _MutationKind.insert, + queryFields, + queryState, + MutationKind.insert, chunk.first, - rows: chunk, - )._compile(selection), + insertRows: chunk, + ).compileQuery(selection), ); chunk = []; parameters = 0; } - for (final row in _rows) { - final actual = row.where((a) => a._value != null).toList(); + for (final row in insertRows) { + final actual = row.where((a) => a.assignedValue != null).toList(); final columns = [for (final a in actual) a.field.definition.name]; - final writer = _Writer( + final writer = SqlWriter( database.dialect, - {_state.source: 't0'}, + {queryState.source: 't0'}, database: database, exactDecimal: database.capabilities.exactDecimal, temporal: database.capabilities.temporal, ); for (final a in actual) { - a._value!.write(writer); + a.assignedValue!.write(writer); } final count = writer.parameters.length; if (count > limit) { @@ -82,13 +109,17 @@ final class BatchInsert { return List.unmodifiable(commands); } - List compile() => _compile(); + /// Returns the SQL chunks without acquiring a connection or inserting rows. + /// + /// A row that alone exceeds the parameter limit is rejected. An empty batch + /// returns an empty command list. + List compile() => compileQuery(); Future _run({ - _SelectionPlan? selection, + SelectionPlan? selection, ExecutionOptions options = const ExecutionOptions(), }) async { options.check(); - final commands = _compile(selection); + final commands = compileQuery(selection); if (commands.isEmpty) return const SqlResult([]); Future execute(QueryContext db) async { var count = 0; @@ -98,7 +129,7 @@ final class BatchInsert { final result = await db.executeCommand( command, options: options, - changedTables: [_state.source.schema], + changedTables: [queryState.source.schema], affectedOnly: true, cascade: false, ); @@ -119,22 +150,45 @@ final class BatchInsert { : database.atomic(execute, acquire: options.acquisition); } + /// Executes all chunks atomically and returns their total affected-row count. + /// + /// An empty batch returns zero without acquiring a connection. Future execute({ ExecutionOptions options = const ExecutionOptions(), }) async => (await _run(options: options)).affectedRows; + + /// Prepares typed scalar projections from every inserted chunk's RETURNING. + /// + /// The connected engine must support RETURNING. Relationships and aggregate + /// or window selections are rejected before executing a chunk. BatchReturning returning(Selection Function(F) selection) => - BatchReturning._(this, selection(_fields)); + BatchReturning.internal(this, selection(queryFields)); } +/// Typed returned rows from a prepared batch insert. +/// +/// Each [get] call executes the batch again. Results are collected across chunks; +/// do not assume that database return order establishes an input-row mapping. final class BatchReturning { final BatchInsert _batch; - final Selection _selection; - BatchReturning._(this._batch, this._selection); + + /// @nodoc + @internal + final Selection querySelection; + + /// @nodoc + @internal + BatchReturning.internal(this._batch, this.querySelection); + + /// Executes every chunk in one transaction and decodes the returned rows. + /// + /// Decoding happens after the batch executes. Use an enclosing transaction + /// when a decoding exception must roll back the inserted data. Future> get({ ExecutionOptions options = const ExecutionOptions(), }) async { - final plan = _SelectionPlan(); - final decode = _selection._bind(plan); + final plan = SelectionPlan(); + final decode = querySelection.bindSelection(plan); final result = await _batch._run(selection: plan, options: options); return _batch.database.observeDecode( null, diff --git a/lib/src/sql_context.dart b/lib/src/query/context.dart similarity index 66% rename from lib/src/sql_context.dart rename to lib/src/query/context.dart index c1882b46..a6918079 100644 --- a/lib/src/sql_context.dart +++ b/lib/src/query/context.dart @@ -1,16 +1,36 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import '../../schema_model.dart'; +import 'query.dart'; +import 'table.dart'; /// Capabilities and an optional execution binding for typed query descriptions. /// The SQL layer knows no connection pool, platform adapter or runtime session. abstract class QueryContext { + /// Creates a context for compiling or executing query descriptions. const QueryContext(); + + /// The selected engine's limits and supported SQL features. Capabilities get capabilities; + + /// The SQL dialect selected by the context's capabilities. SqlDialect get dialect => capabilities.dialect; + + /// Whether this view borrows an active transaction. bool get inTransaction => false; + + /// Whether this view borrows one leased connection. bool get inSession => false; + /// Binds a manual or generated table definition to this context. TableSet table(Table definition) => TableSet(this, definition); + + /// Registers foreign-key effects for change subscriptions. + /// + /// Generated table getters call this hook. Compilation-only contexts need no + /// subscription state, so the default implementation does nothing. void registerSchema(Iterable tables) {} Never _unbound() => throw const OrmException( @@ -18,20 +38,29 @@ abstract class QueryContext { 'This query only describes SQL. Bind it to a Database to execute it.', ); + /// Executes work on a leased connection, or rejects an unbound context. Future run( Future Function(SqlConnection) action, { AcquisitionOptions acquire = const AcquisitionOptions(), }) => _unbound(); + + /// @nodoc + @internal Future executeOn( SqlConnection connection, SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), }) => _unbound(); + + /// Executes bound SQL and declares tables affected by raw writes. Future execute( SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), Iterable changedTables = const [], }) => _unbound(); + + /// @nodoc + @internal Future executeCommand( SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), @@ -39,10 +68,16 @@ abstract class QueryContext { bool affectedOnly = false, bool cascade = true, }) => _unbound(); + + /// @nodoc + @internal Future atomic( Future Function(QueryContext) action, { AcquisitionOptions acquire = const AcquisitionOptions(), }) => _unbound(); + + /// @nodoc + @internal Stream streamRows( SqlCommand command, { required int batchSize, @@ -54,8 +89,14 @@ abstract class QueryContext { ) decode, }) => _unbound(); + + /// @nodoc + @internal T observeDecode(String? sql, int inputRows, T Function() action) => action(); + + /// @nodoc + @internal void markFailed() => _unbound(); } @@ -63,7 +104,13 @@ abstract class QueryContext { final class SqlBuilder extends QueryContext { @override final Capabilities capabilities; + + /// Builds SQL using an explicitly supplied engine capability profile. const SqlBuilder.withCapabilities(this.capabilities); + + /// Builds SQL using the dialect's offline defaults. + /// + /// Use withCapabilities when an adapter exposes stricter live limits. SqlBuilder(SqlDialect dialect) : capabilities = Capabilities( dialect: dialect, diff --git a/lib/src/query/cte.dart b/lib/src/query/cte.dart new file mode 100644 index 00000000..fa60a88d --- /dev/null +++ b/lib/src/query/cte.dart @@ -0,0 +1,177 @@ +import 'package:meta/meta.dart'; + +import '../../schema_model.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; +import 'union.dart'; + +/// @nodoc +@internal +abstract interface class CteDefinition { + String get name; + String writeDefinition(SqlWriter writer); +} + +/// A CTE exports SQL expression identities. Dart mapper properties are not +/// mistaken for SQL columns. ref() accepts only expressions the source selected. +/// +/// Create one with [Query.asCte]. Accessing [query] or [alias] only builds a +/// description; database work starts when the resulting query executes. +final class Cte implements CteDefinition { + final Query _source; + + /// SQL name used for this common table expression. + @override + final String name; + + /// @nodoc + @internal + final SelectionPlan planQuery; + final RowDecoder _decode; + late final Table> _table = _makeTable(); + + /// @nodoc + @internal + factory Cte.internal(Query source, String name) { + if (name.isEmpty) throw ArgumentError('CTE name cannot be empty.'); + final (plan, decode) = source.planQuery( + deduplicate: sqlRowShape(source.querySelection) == null, + ); + if (plan.relations.isNotEmpty) { + throw const OrmException( + 'QUERY.CTE', + 'A CTE contains SQL columns, not batch-loaded relations.', + ); + } + return Cte._planned(source, name, plan, decode); + } + Cte._planned(this._source, this.name, this.planQuery, this._decode); + + Table> _makeTable() { + final optional = { + for (final join in [..._source.queryState.joins, ...planQuery.joins]) + if (join.left) join.alias.fields.table, + }; + final nullable = [ + for (final column in planQuery.columns) + column.codec.acceptsNull || + outerNullable(column.expressionNode, optional), + ]; + final schema = TableSchema( + name, + columns: [ + for (var i = 0; i < planQuery.columns.length; i++) + Column( + 'c$i', + nullable[i] + ? planQuery.columns[i].codec.nullable() + : planQuery.columns[i].codec, + nullable: nullable[i], + ), + ], + ); + return Table( + schema, + (table) => + CteFields.internal(table, _source.queryFields, planQuery, nullable), + (fields) => ReboundSelection(_decode, [ + for (var i = 0; i < planQuery.columns.length; i++) + Expr.internal( + ColumnNode(fields.table, 'c$i'), + schema.columns[i].codec, + ), + ], source: _source.querySelection), + ); + } + + /// Starts a query over the CTE while retaining the source's decoded row type. + /// + /// The result belongs to the same database or transaction view as its source. + Query> get query { + final fields = _table.createFields(TableRef(_table.schema)); + return Query.internal( + _source.database, + fields, + QueryState(fields.table, ctes: [this]), + _table.selectRow(fields), + ); + } + + /// Creates a fresh CTE occurrence for a join or self join. + TableAlias> alias() => TableAlias.internal(_table, this); + + /// @nodoc + @internal + @override + String writeDefinition(SqlWriter writer) => + '${writer.quote(name)} ' + '(${[for (var i = 0; i < planQuery.columns.length; i++) writer.quote('c$i')].join(', ')}) ' + 'AS (${_source.writeQuery(writer, planQuery)})'; +} + +/// SQL columns exported by a CTE, referenced through its original expressions. +final class CteFields extends Fields { + /// @nodoc + @internal + final F sourceFields; + + /// @nodoc + @internal + final SelectionPlan planQuery; + final List _nullable; + + /// @nodoc + @internal + CteFields.internal( + super.table, + this.sourceFields, + this.planQuery, + this._nullable, + ); + + /// References an expression selected by the CTE's source query. + /// + /// The expression must match an exported SQL column. Columns made nullable by + /// a left join require a nullable reference; Dart mapper properties are not + /// SQL columns and cannot be referenced here. + Expr ref(Expr Function(F) expression) { + final original = expression(sourceFields); + final index = planQuery.columns.indexWhere( + (e) => sameSqlNode(e.expressionNode, original.expressionNode), + ); + if (index < 0) { + throw const OrmException( + 'QUERY.CTE_COLUMN', + 'The CTE did not export this SQL expression.', + ); + } + if (_nullable[index] && !original.codec.acceptsNull) { + throw const OrmException( + 'QUERY.NULLABILITY', + 'This CTE column is nullable; reference it with a nullable expression.', + ); + } + return Expr.internal(ColumnNode(table, 'c$index'), original.codec); + } +} + +/// @nodoc +@internal +final class ReboundSelection( + final RowDecoder decode, + final List> columns, { + final Selection? source, +}) extends Selection { + @override + RowDecoder bindSelection(SelectionPlan plan) { + for (final column in columns) { + plan.require(column); + } + final indices = [for (final column in columns) plan.column(column)]; + return (row) => decode([for (final index in indices) row[index]]); + } +} diff --git a/lib/src/cursor.dart b/lib/src/query/cursor.dart similarity index 60% rename from lib/src/cursor.dart rename to lib/src/query/cursor.dart index dd2375d0..a146951d 100644 --- a/lib/src/cursor.dart +++ b/lib/src/query/cursor.dart @@ -1,58 +1,90 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; +import 'dart:convert'; +import 'dart:typed_data'; + +import '../../driver.dart'; +import '../../schema_model.dart'; +import 'expression.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'table.dart'; + +/// Explicit placement of SQL NULL relative to non-null sort values. enum NullOrder { first, last } +/// One ordering expression paired with its last-seen, encoded cursor value. +/// +/// Create terms with a typed expression's `cursor` method. Nullable expressions +/// require an explicit [NullOrder] when used for keyset pagination. final class CursorTerm { + /// SQL ordering whose value is carried by this cursor term. final OrderTerm order; - final Object? _value; - const CursorTerm._(this.order, this._value); + + /// @nodoc + @internal + final Object? assignedValue; + + /// @nodoc + @internal + const CursorTerm.internal(this.order, this.assignedValue); } +/// Stable keyset pagination for a filtered table query. +/// +/// Sort columns must include a non-null declared primary or unique key. Joins, +/// grouping, DISTINCT, offsets, CTEs, and UNION queries cannot use this pagination. extension KeysetQuery on Query { + /// Adds a lexicographic boundary after [cursor] and replaces query ordering. + /// + /// Existing WHERE filters remain in effect. Cursor terms must use distinct + /// columns from this table, with explicit NULL ordering for nullable columns. Query seekAfter(List Function(F) cursor) { - final terms = List.unmodifiable(cursor(_fields)); + final terms = List.unmodifiable(cursor(queryFields)); _validateCursor(terms.map((t) => t.order).toList()); - _Node? predicate; - _Node? prefix; + SqlNode? predicate; + SqlNode? prefix; for (final term in terms) { - final node = term.order.expression._node; - final last = term._value; - final isNull = _Unary('IS NULL', node, postfix: true); - _Node comparison; + final node = term.order.expression.expressionNode; + final last = term.assignedValue; + final isNull = UnaryNode('IS NULL', node, postfix: true); + SqlNode comparison; if (last == null) { comparison = term.order.nulls == NullOrder.first - ? _Unary('IS NOT NULL', node, postfix: true) - : _Raw(const ['FALSE'], const []); + ? UnaryNode('IS NOT NULL', node, postfix: true) + : RawNode(const ['FALSE'], const []); } else { - comparison = _Binary( + comparison = BinaryNode( node, term.order.descending ? '<' : '>', - _Parameter(last, storageType: term.order.expression.codec.sqlType), + ParameterNode(last, storageType: term.order.expression.codec.sqlType), ); if (term.order.nulls == NullOrder.last) { - comparison = _Binary(comparison, 'OR', isNull); + comparison = BinaryNode(comparison, 'OR', isNull); } } final branch = prefix == null ? comparison - : _Binary(prefix, 'AND', comparison); - predicate = predicate == null ? branch : _Binary(predicate, 'OR', branch); + : BinaryNode(prefix, 'AND', comparison); + predicate = predicate == null + ? branch + : BinaryNode(predicate, 'OR', branch); final equal = last == null ? isNull - : _Binary( + : BinaryNode( node, '=', - _Parameter( + ParameterNode( last, storageType: term.order.expression.codec.sqlType, ), ); - prefix = prefix == null ? equal : _Binary(prefix, 'AND', equal); + prefix = prefix == null ? equal : BinaryNode(prefix, 'AND', equal); } - final next = Expr._(predicate!, Codecs.boolean.nullable()); - return _copy( - _state.copy( - predicate: _state.predicate?.and(next) ?? next, + final next = Expr.internal(predicate!, Codecs.boolean.nullable()); + return copyQuery( + queryState.copy( + predicate: queryState.predicate?.and(next) ?? next, order: [for (final term in terms) term.order], ), ); @@ -61,29 +93,36 @@ extension KeysetQuery on Query { /// Versioned, lossless cursor transport. This is pagination input, not an /// authorization token. The next query still applies its own WHERE policies. String cursorToken(List Function(F) cursor) { - final terms = cursor(_fields); + final terms = cursor(queryFields); _validateCursor(terms.map((t) => t.order).toList()); final payload = { 'version': 1, - 'table': _state.source.schema.name, + 'table': queryState.source.schema.name, 'order': _cursorShape(terms.map((t) => t.order).toList()), - 'values': [for (final term in terms) _encodeCursorValue(term._value)], + 'values': [ + for (final term in terms) _encodeCursorValue(term.assignedValue), + ], }; return base64Url.encode(utf8.encode(jsonEncode(payload))); } + /// Restores a [cursorToken] using the expected table and complete sort order. + /// + /// Rejects malformed tokens or mismatched columns, codecs, directions, and + /// nullability with `QUERY.CURSOR`. Existing filters still apply; the token + /// is neither signed authorization nor a database snapshot. Query seekToken( String token, { required List Function(F) orderBy, }) { - final order = orderBy(_fields); + final order = orderBy(queryFields); _validateCursor(order); try { final payload = jsonDecode( utf8.decode(base64Url.decode(token)), ) as Map; if (payload['version'] != 1 || - payload['table'] != _state.source.schema.name || + payload['table'] != queryState.source.schema.name || jsonEncode(payload['order']) != jsonEncode(_cursorShape(order))) { throw const FormatException( 'Cursor belongs to another schema or sort order.', @@ -97,7 +136,7 @@ extension KeysetQuery on Query { for (var i = 0; i < order.length; i++) { final codec = order[i].expression.codec; final decoded = codec.decode(_decodeCursorValue(values[i])); - terms.add(CursorTerm._(order[i], codec.encode(decoded))); + terms.add(CursorTerm.internal(order[i], codec.encode(decoded))); } return seekAfter((_) => terms); } catch (e) { @@ -111,12 +150,12 @@ extension KeysetQuery on Query { void _validateCursor(List order) { if (order.isEmpty || - _state.joins.isNotEmpty || - _state.group.isNotEmpty || - _state.distinct || - _state.offset != null || - _state.union != null || - _state.ctes.isNotEmpty) { + queryState.joins.isNotEmpty || + queryState.group.isNotEmpty || + queryState.distinct || + queryState.offset != null || + queryState.union != null || + queryState.ctes.isNotEmpty) { throw const OrmException( 'QUERY.CURSOR', 'Keyset pagination requires an ungrouped table query without joins, DISTINCT or offset.', @@ -124,9 +163,9 @@ extension KeysetQuery on Query { } final names = {}; for (final term in order) { - final node = _unwrapStorage(term.expression._node); - if (node is! _ColumnNode || - node.table != _state.source || + final node = unwrapStorage(term.expression.expressionNode); + if (node is! ColumnNode || + node.table != queryState.source || !names.add(node.name)) { throw const OrmException( 'QUERY.CURSOR', @@ -140,7 +179,7 @@ extension KeysetQuery on Query { ); } } - final schema = _state.source.schema; + final schema = queryState.source.schema; final keys = [ schema.primaryKey, ...schema.uniqueKeys, @@ -166,7 +205,7 @@ extension KeysetQuery on Query { List _cursorShape(List order) => [ for (final o in order) [ - (_unwrapStorage(o.expression._node) as _ColumnNode).name, + (unwrapStorage(o.expression.expressionNode) as ColumnNode).name, o.expression.codec.sqlType, o.expression.codec.acceptsNull, o.descending, @@ -187,6 +226,7 @@ Object? _encodeCursorValue(Object? value) => switch (value) { 'Codec storage value is not supported in cursors.', ), }; + Object? _decodeCursorValue(Object? value) => switch (value) { null || String() || bool() => value, ['int', String v] => _cursorInt(v), diff --git a/lib/src/query/expression.dart b/lib/src/query/expression.dart new file mode 100644 index 00000000..e3426d8c --- /dev/null +++ b/lib/src/query/expression.dart @@ -0,0 +1,480 @@ +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import '../../schema_model.dart'; +import 'cursor.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; + +/// A typed SQL expression that can also be selected as a result. +/// +/// Operators build SQL without executing it. The codec controls bound values and +/// result decoding; nullable predicates retain SQL three-valued logic. +class Expr extends Selection { + /// @nodoc + @internal + final SqlNode expressionNode; + + /// Encodes parameters and decodes this expression's result column. + final Codec codec; + + /// @nodoc + @internal + Expr.internal(SqlNode node, this.codec) + : expressionNode = switch (codec.sqlType) { + 'decimal' when node is! DecimalNode => DecimalNode(node), + 'date' || 'time' || 'local_datetime' || 'instant' + when node is! TemporalNode => + TemporalNode(node, codec.sqlType), + _ => node, + }; + + /// Compares with a bound value; null produces SQL `IS NULL`. + Expr eq(T value) => value == null + ? Expr.internal( + UnaryNode('IS NULL', expressionNode, postfix: true), + Codecs.boolean.nullable(), + ) + : _compare( + '=', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + + /// Compares with a bound value; null produces SQL `IS NOT NULL`. + Expr ne(T value) => value == null + ? Expr.internal( + UnaryNode('IS NOT NULL', expressionNode, postfix: true), + Codecs.boolean.nullable(), + ) + : _compare( + '<>', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + + /// Compares two SQL expressions using `=` and SQL NULL semantics. + Expr equals(Expr other) => _compare('=', other.expressionNode); + + /// Tests whether this value is greater than the bound value. + Expr gt(T value) => _compare( + '>', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + + /// Tests whether this value is greater than or equal to the bound value. + Expr gte(T value) => _compare( + '>=', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + + /// Tests whether this value is less than the bound value. + Expr lt(T value) => _compare( + '<', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + + /// Tests whether this value is less than or equal to the bound value. + Expr lte(T value) => _compare( + '<=', + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ); + Expr _compare(String op, SqlNode right) => Expr.internal( + BinaryNode(expressionNode, op, right), + Codecs.boolean.nullable(), + ); + + /// Tests for SQL NULL, yielding a non-null boolean. + Expr isNull() => Expr.internal( + UnaryNode('IS NULL', expressionNode, postfix: true), + Codecs.boolean, + ); + + /// Tests for a non-NULL value, yielding a non-null boolean. + Expr isNotNull() => Expr.internal( + UnaryNode('IS NOT NULL', expressionNode, postfix: true), + Codecs.boolean, + ); + + /// Tests membership in separately bound values; an empty list is false. + Expr isIn(Iterable values) => Expr.internal( + InNode(expressionNode, [ + for (final value in values) + ParameterNode(codec.encode(value), storageType: codec.sqlType), + ]), + Codecs.boolean.nullable(), + ); + + /// Tests membership in a subquery selecting one SQL expression. + /// + /// The subquery must belong to the enclosing query's context and dialect. + Expr isInQuery(Query query) { + if (query.querySelection is! Expr) { + throw const OrmException( + 'QUERY.SCALAR', + 'IN subqueries select one SQL expression.', + ); + } + return Expr.internal( + BinaryNode(expressionNode, 'IN', SubqueryNode(query)), + Codecs.boolean.nullable(), + ); + } + + /// Turns an aggregate into a window expression. + /// + /// Partition and ordering expressions cannot contain nested windows. The selected + /// database must support window functions. + Expr over({ + List> partitionBy = const [], + List orderBy = const [], + WindowFrame? frame, + }) { + final function = unwrapStorage(expressionNode); + if ((function is! FunctionNode && function is! DecimalAverage) || + !aggregate(function)) { + throw const OrmException( + 'QUERY.WINDOW', + 'over() applies to an aggregate expression.', + ); + } + if ([ + ...partitionBy.map((e) => e.expressionNode), + ...orderBy.map((o) => o.expression.expressionNode), + ].any(window)) { + throw const OrmException( + 'QUERY.WINDOW', + 'Window partition/order expressions cannot contain another window.', + ); + } + return Expr.internal( + WindowNode( + function, + List.unmodifiable(partitionBy), + List.unmodifiable(orderBy), + frame, + ), + codec, + ); + } + + /// Orders ascending, optionally specifying where NULL values appear. + OrderTerm asc({NullOrder? nulls}) => OrderTerm.internal(this, false, nulls); + + /// Orders descending, optionally specifying where NULL values appear. + OrderTerm desc({NullOrder? nulls}) => OrderTerm.internal(this, true, nulls); + + /// Builds a typed keyset boundary from this expression and a row value. + CursorTerm cursor(T value, {bool descending = false, NullOrder? nulls}) => + CursorTerm.internal( + OrderTerm.internal(this, descending, nulls), + codec.encode(value), + ); + + /// Counts non-NULL values, optionally counting only distinct values. + Expr count({bool distinct = false}) => Expr.internal( + FunctionNode('COUNT', [expressionNode], distinct: distinct), + Codecs.integer, + ); + + /// Selects the minimum value, or null for an empty group. + Expr min() => + Expr.internal(FunctionNode('MIN', [expressionNode]), codec.nullable()); + + /// Selects the maximum value, or null for an empty group. + Expr max() => + Expr.internal(FunctionNode('MAX', [expressionNode]), codec.nullable()); + + /// @nodoc + @internal + @override + RowDecoder bindSelection(SelectionPlan plan) { + plan.require(this); + final index = plan.column(this); + return (row) => codec.decode(row[index]); + } +} + +/// Creates a bound SQL value using an explicit storage codec. +Expr value(T value, Codec codec) => Expr.internal( + ParameterNode(codec.encode(value), sqlType: codec.sqlType), + codec, +); + +/// [parts] are trusted SQL, [values] are expressions. Never put user input in parts. +Expr sql(List parts, List> values, Codec codec) { + if (parts.length != values.length + 1) { + throw ArgumentError('SQL needs one more text part than expression.'); + } + for (final part in parts) { + checkSqlText(part); + } + return Expr.internal( + RawNode(List.of(parts), [for (final v in values) v.expressionNode]), + codec, + ); +} + +/// Boolean composition using SQL's three-valued NULL semantics. +extension Predicate on Expr { + /// Requires both predicates to be true. + Expr and(Expr other) => Expr.internal( + BinaryNode(expressionNode, 'AND', other.expressionNode), + Codecs.boolean.nullable(), + ); + + /// Requires either predicate to be true. + Expr or(Expr other) => Expr.internal( + BinaryNode(expressionNode, 'OR', other.expressionNode), + Codecs.boolean.nullable(), + ); + + /// Negates this predicate; SQL NULL remains unknown. + Expr not() => Expr.internal( + UnaryNode('NOT', expressionNode), + Codecs.boolean.nullable(), + ); +} + +/// String operations evaluated by the selected database. +extension TextExpression on Expr { + /// Matches a bound SQL LIKE pattern; `%` and `_` remain wildcards. + Expr like(String pattern) => _compare('LIKE', ParameterNode(pattern)); + + /// Converts text to lowercase using database rules. + Expr lower() => + Expr.internal(FunctionNode('LOWER', [expressionNode]), Codecs.text); + + /// Converts text to uppercase using database rules. + Expr upper() => + Expr.internal(FunctionNode('UPPER', [expressionNode]), Codecs.text); +} + +/// SQL arithmetic and aggregation for Dart numeric values. +extension NumericExpression on Expr { + /// Adds a bound value in SQL. + Expr plus(T n) => Expr.internal( + BinaryNode( + expressionNode, + '+', + ParameterNode(codec.encode(n), storageType: codec.sqlType), + ), + codec, + ); + + /// Subtracts a bound value in SQL. + Expr minus(T n) => Expr.internal( + BinaryNode( + expressionNode, + '-', + ParameterNode(codec.encode(n), storageType: codec.sqlType), + ), + codec, + ); + + /// Multiplies by a bound value in SQL. + Expr times(T n) => Expr.internal( + BinaryNode( + expressionNode, + '*', + ParameterNode(codec.encode(n), storageType: codec.sqlType), + ), + codec, + ); + + /// Sums values, returning null for an empty group. + Expr sum() => + Expr.internal(FunctionNode('SUM', [expressionNode]), codec.nullable()); + + /// Computes a floating-point SQL average, or null for an empty group. + Expr average() => Expr.internal( + FunctionNode('AVG', [expressionNode]), + Codecs.real.nullable(), + ); +} + +/// An expression's sort direction and optional NULL ordering policy. +final class OrderTerm { + /// The SQL expression to order by. + final Expr expression; + + /// Whether larger values appear first. + final bool descending; + + /// Explicit NULL ordering, or null to use the database default. + final NullOrder? nulls; + + /// @nodoc + @internal + const OrderTerm.internal(this.expression, this.descending, [this.nulls]); + + /// @nodoc + @internal + String writeQuery(SqlWriter writer) { + final text = expression.expressionNode.write(writer); + if (writer.mysql && nulls != null) { + return '($text IS NULL) ${nulls == NullOrder.first ? 'DESC' : 'ASC'}, ' + '${expression.expressionNode.write(writer)} ${descending ? 'DESC' : 'ASC'}'; + } + return '$text$orderSuffix'; + } + + /// @nodoc + @internal + String get orderSuffix => + ' ${descending ? 'DESC' : 'ASC'}' + '${nulls == null ? '' : ' NULLS ${nulls!.name.toUpperCase()}'}'; +} + +/// @nodoc +@internal +String projection(Expr expression, SqlWriter writer) { + final text = expression.expressionNode.write(writer); + // Preserve JSON null separately from SQL NULL through native driver decoders. + return writer.mysql && expression.codec.sqlType == 'json' + ? 'CAST($text AS CHAR CHARACTER SET utf8mb4)' + : text; +} + +/// @nodoc +@internal +void checkTemporalPrecision(int digits) { + if (digits < 0 || digits > 6) throw RangeError.range(digits, 0, 6, 'digits'); +} + +/// Precision control for SQL time expressions. +extension TimeExpression on Expr { + /// Rounds to the requested fractional-second digits (0 through 6). + Expr withPrecision(int digits) { + checkTemporalPrecision(digits); + return Expr.internal(TemporalCast(expressionNode, 'time', digits), codec); + } +} + +/// Precision control for local timestamps without a time zone. +extension LocalDateTimeExpression on Expr { + /// Rounds to the requested fractional-second digits (0 through 6). + Expr withPrecision(int digits) { + checkTemporalPrecision(digits); + return Expr.internal( + TemporalCast(expressionNode, 'local_datetime', digits), + codec, + ); + } +} + +/// Precision control for UTC instant expressions. +extension InstantExpression on Expr { + /// Rounds to the requested fractional-second digits (0 through 6). + Expr withPrecision(int digits) { + checkTemporalPrecision(digits); + return Expr.internal( + TemporalCast(expressionNode, 'instant', digits), + codec, + ); + } +} + +/// Exact decimal arithmetic using the selected database's capabilities. +extension DecimalExpression on Expr { + /// Averages non-null inputs, rounding once at the requested result scale. + /// Empty/all-null input returns null; the default rejects lost digits. + Expr average({ + required int scale, + DecimalRounding rounding = DecimalRounding.exact, + }) { + Decimal.validateScale(scale); + if (window(expressionNode)) { + throw const OrmException( + 'QUERY.WINDOW', + 'Project a window result through a CTE before averaging it.', + ); + } + return Expr.internal( + DecimalAverage(expressionNode, scale, rounding), + Codecs.decimal.nullable(), + ); + } + + /// Rounds in SQL; [DecimalRounding.exact] rejects any lost nonzero digits. + Expr rounded( + int scale, { + DecimalRounding rounding = DecimalRounding.exact, + }) { + Decimal.validateScale(scale); + return Expr.internal( + DecimalRatio(expressionNode, null, scale, rounding), + codec, + ); + } + + /// Divides in SQL to an explicit scale without a floating-point intermediate. + Expr divide( + Decimal divisor, { + required int scale, + DecimalRounding rounding = DecimalRounding.exact, + }) { + Decimal.validateScale(scale); + return Expr.internal( + DecimalRatio( + expressionNode, + value(divisor, Codecs.decimal).expressionNode, + scale, + rounding, + ), + codec, + ); + } + + /// Divides two SQL expressions. A null operand produces null. + Expr divideExpression( + Expr divisor, { + required int scale, + DecimalRounding rounding = DecimalRounding.exact, + }) { + Decimal.validateScale(scale); + return Expr.internal( + DecimalRatio(expressionNode, divisor.expressionNode, scale, rounding), + Codecs.decimal.nullable(), + ); + } + + /// Rounds and checks a decimal against the requested precision and scale. + Expr constrained(int precision, int scale) { + Decimal.validateDigits(precision, scale); + return Expr.internal(DecimalCast(expressionNode, precision, scale), codec); + } + + /// Adds an exactly encoded decimal value. + Expr plus(Decimal n) => _arithmetic('+', value(n, Codecs.decimal)); + + /// Subtracts an exactly encoded decimal value. + Expr minus(Decimal n) => _arithmetic('-', value(n, Codecs.decimal)); + + /// Multiplies by an exactly encoded decimal value. + Expr times(Decimal n) => _arithmetic('*', value(n, Codecs.decimal)); + + /// Adds another decimal SQL expression. + Expr plusExpression(Expr other) => _binary('+', other); + + /// Subtracts another decimal SQL expression. + Expr minusExpression(Expr other) => _binary('-', other); + + /// Multiplies by another decimal SQL expression. + Expr timesExpression(Expr other) => _binary('*', other); + Expr _arithmetic(String op, Expr other) => Expr.internal( + DecimalArithmetic(expressionNode, op, other.expressionNode), + codec, + ); + Expr _binary(String op, Expr other) => Expr.internal( + DecimalArithmetic(expressionNode, op, other.expressionNode), + Codecs.decimal.nullable(), + ); + + /// Sums decimal values exactly, returning null for an empty group. + Expr sum({bool distinct = false}) => Expr.internal( + FunctionNode('SUM', [expressionNode], decimal: true, distinct: distinct), + Codecs.decimal.nullable(), + ); +} diff --git a/lib/src/query/joins.dart b/lib/src/query/joins.dart new file mode 100644 index 00000000..70bc8a40 --- /dev/null +++ b/lib/src/query/joins.dart @@ -0,0 +1,365 @@ +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import '../../schema_model.dart'; +import 'cte.dart'; +import 'expression.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; + +/// Explicit aliases support self joins and any number of joins without adding +/// a growing number of generic join-result types. +final class TableAlias { + /// Physical table and row decoder represented by this occurrence. + final Table definition; + + /// Fields scoped to this occurrence, independent of another alias of the table. + final F fields; + + /// @nodoc + @internal + final CteDefinition? cteDefinition; + + /// @nodoc + @internal + late final String presenceMarker = _presenceName(); + + /// @nodoc + @internal + TableAlias.internal(this.definition, [this.cteDefinition]) + : fields = definition.createFields(TableRef(definition.schema)); + String _presenceName() { + var name = '_orm_present'; + while (definition.schema.columns.any((c) => c.name.toLowerCase() == name)) { + name += '_'; + } + return name; + } + + /// Decodes [selection] only when this alias is present in a joined row. + /// + /// A missing left-joined row produces null. This guard establishes presence + /// only for this alias; independently optional aliases need their own guards. + Selection optional(Selection selection) => + _OptionalJoin(this, selection); + + /// Selects the complete model row, or null when this alias is absent. + Selection get row => optional(definition.selectRow(fields)); + + /// Decodes a SQL expression as nullable, including an absent left-join value. + /// + /// This changes the result codec without adding an SQL presence test. + Expr nullable(Expr expression) => + Expr.internal(expression.expressionNode, expression.codec.nullable()); + + /// SQL expression testing whether this joined occurrence has a row. + Expr get isPresent => + Expr.internal(PresenceNode(this), Codecs.integer.nullable()).isNotNull(); +} + +/// @nodoc +@internal +final class Join( + final TableAlias alias, + final Expr on, + final bool left, +); + +final class _OptionalJoin( + final TableAlias alias, + final Selection selection, +) extends Selection { + /// @nodoc + @internal + @override + RowDecoder bindSelection(SelectionPlan plan) { + final marker = plan.column( + Expr.internal(PresenceNode(alias), Codecs.integer.nullable()), + ); + final decode = plan.optional(alias.fields.table, selection); + return (row) => row[marker] == null ? null : decode(row); + } +} + +/// Frame used by a window aggregate. +/// +/// [rowsToCurrent] includes rows from the partition's start through this row; +/// [rowsAll] includes the whole partition. [rangeToCurrent] also includes peers +/// equal under the window ordering. +enum WindowFrame { rowsToCurrent, rowsAll, rangeToCurrent } + +/// Builds a one-based SQL row number within each partition. +/// +/// Supply [orderBy] for deterministic numbering; this does not order the final +/// query result unless that query also declares its own ordering. +Expr rowNumber({ + List> partitionBy = const [], + List orderBy = const [], +}) => Expr.internal( + WindowNode( + FunctionNode('ROW_NUMBER', const []), + List.unmodifiable(partitionBy), + List.unmodifiable(orderBy), + null, + ), + Codecs.integer, +); + +/// Builds one-based SQL rank, with ties sharing a rank and leaving later gaps. +/// +/// Partition and ordering expressions belong to the surrounding query scope. +Expr rank({ + List> partitionBy = const [], + required List orderBy, +}) => Expr.internal( + WindowNode( + FunctionNode('RANK', const []), + List.unmodifiable(partitionBy), + List.unmodifiable(orderBy), + null, + ), + Codecs.integer, +); + +/// @nodoc +@internal +bool aggregate(SqlNode node) => switch (node) { + WindowNode() || SubqueryNode() || RelationSubqueryNode() => false, + DecimalAverage() => true, + FunctionNode(:final name) + when {'COUNT', 'SUM', 'MIN', 'MAX', 'AVG'}.contains(name) => + true, + _ => children(node).any(aggregate), +}; + +/// @nodoc +@internal +bool window(SqlNode node) => node is WindowNode || children(node).any(window); + +/// @nodoc +@internal +List children(SqlNode node) => switch (node) { + TemporalNode(:final child) || TemporalCast(:final child) => [child], + DecimalAverage(:final child) || AverageInput(:final child) => [child], + DecimalRatio(:final numerator, :final divisor) => [numerator, ?divisor], + DecimalNode(:final child) => [child], + DecimalCast(:final child) => [child], + DecimalArithmetic(:final left, :final right) => [left, right], + BinaryNode(:final left, :final right) => [left, right], + UnaryNode(:final child) => [child], + FunctionNode(:final arguments) => arguments, + InNode(:final expression, :final values) => [expression, ...values], + RawNode(:final values) => values, + WindowNode(:final function, :final partition, :final order) => [ + function, + ...partition.map((e) => e.expressionNode), + ...order.map((e) => e.expression.expressionNode), + ], + RelationSubqueryNode(:final relation) => [ + for (final p in relation.parentFields) p.expressionNode, + ], + _ => [], +}; + +/// @nodoc +@internal +void validateGrouping(QueryState state, SelectionPlan plan) { + final optional = { + for (final join in state.joins) + if (join.left) join.alias.fields.table, + }; + bool unguarded(MapEntry, List>> entry) { + final absent = optional.difference(entry.key); + return entry.value.any((e) => outerNullable(e.expressionNode, absent)); + } + + if (plan.required.any((e) => outerNullable(e.expressionNode, optional)) || + plan.guarded.entries.any(unguarded)) { + throw const OrmException( + 'QUERY.NULLABILITY', + 'Use alias.optional(selection) or alias.nullable(expression) for outer-joined fields.', + ); + } + final predicates = [ + if (state.predicate case final p?) p.expressionNode, + for (final j in state.joins) j.on.expressionNode, + ]; + if (predicates.any((p) => aggregate(p) || window(p))) { + throw const OrmException( + 'QUERY.AGGREGATE', + 'WHERE and JOIN predicates cannot contain aggregate or window functions.', + ); + } + if (state.group.any( + (e) => aggregate(e.expressionNode) || window(e.expressionNode), + ) || + (state.having != null && window(state.having!.expressionNode))) { + throw const OrmException( + 'QUERY.AGGREGATE', + 'Invalid aggregate/window placement.', + ); + } + final checked = [ + for (final e in plan.columns) e.expressionNode, + for (final o in state.order) o.expression.expressionNode, + if (state.having case final h?) h.expressionNode, + ]; + if (state.group.isEmpty && !checked.any(aggregate)) { + if (state.having != null) { + throw const OrmException( + 'QUERY.AGGREGATE', + 'HAVING requires grouping or aggregation.', + ); + } + return; + } + bool grouped(SqlNode node) { + if (state.group.any((g) => sameSqlNode(g.expressionNode, node))) { + return true; + } + if (node is DecimalAverage) return true; + if (node is FunctionNode && + {'COUNT', 'SUM', 'MIN', 'MAX', 'AVG'}.contains(node.name)) { + return true; + } + if (node is ColumnNode || node is PresenceNode) return false; + if (node is WindowNode) { + return [ + ...children(node.function), + ...node.partition.map((e) => e.expressionNode), + ...node.order.map((e) => e.expression.expressionNode), + ].every(grouped); + } + return children(node).every(grouped); + } + + if (!checked.every(grouped)) { + throw const OrmException( + 'QUERY.GROUPING', + 'Select grouped expressions or aggregates; ungrouped columns are ambiguous.', + ); + } +} + +/// @nodoc +@internal +bool outerNullable(SqlNode node, Set optional) => switch (node) { + ColumnNode(:final table) => optional.contains(table), + WindowNode() || RawNode() => false, + FunctionNode(name: 'COUNT') => false, + UnaryNode(op: 'IS NULL' || 'IS NOT NULL') => false, + _ => children(node).any((child) => outerNullable(child, optional)), +}; + +/// @nodoc +@internal +bool sameSqlNode(SqlNode a, SqlNode b) { + if (identical(a, b)) return true; + if (a is WindowNode && b is WindowNode) { + return a.frame == b.frame && + sameSqlNode(a.function, b.function) && + a.partition.length == b.partition.length && + a.partition.indexed.every( + (e) => sameSqlNode( + e.$2.expressionNode, + b.partition[e.$1].expressionNode, + ), + ) && + a.order.length == b.order.length && + a.order.indexed.every((e) { + final other = b.order[e.$1]; + return e.$2.descending == other.descending && + e.$2.nulls == other.nulls && + sameSqlNode( + e.$2.expression.expressionNode, + other.expression.expressionNode, + ); + }); + } + return switch ((a, b)) { + ( + TemporalNode(child: final ac, kind: final ak), + TemporalNode(child: final bc, kind: final bk), + ) => + ak == bk && sameSqlNode(ac, bc), + ( + DecimalAverage(child: final ac, scale: final ax, rounding: final ar), + DecimalAverage(child: final bc, scale: final bx, rounding: final br), + ) => + ax == bx && ar == br && sameSqlNode(ac, bc), + ( + DecimalRatio( + numerator: final an, + divisor: final ad, + scale: final ax, + rounding: final ar, + ), + DecimalRatio( + numerator: final bn, + divisor: final bd, + scale: final bx, + rounding: final br, + ), + ) => + ax == bx && + ar == br && + sameSqlNode(an, bn) && + (ad == null ? bd == null : bd != null && sameSqlNode(ad, bd)), + ( + DecimalCast(child: final ac, precision: final ap, scale: final asc), + DecimalCast(child: final bc, precision: final bp, scale: final bs), + ) => + ap == bp && asc == bs && sameSqlNode(ac, bc), + ( + TemporalCast(child: final ac, kind: final ak, digits: final ap), + TemporalCast(child: final bc, kind: final bk, digits: final bp), + ) => + ak == bk && ap == bp && sameSqlNode(ac, bc), + (DecimalNode(child: final ac), DecimalNode(child: final bc)) => sameSqlNode( + ac, + bc, + ), + ( + DecimalArithmetic(left: final al, op: final ao, right: final ar), + DecimalArithmetic(left: final bl, op: final bo, right: final br), + ) => + ao == bo && sameSqlNode(al, bl) && sameSqlNode(ar, br), + ( + ColumnNode(table: final at, name: final an), + ColumnNode(table: final bt, name: final bn), + ) => + at == bt && an == bn, + ( + ParameterNode(value: final av, sqlType: final at), + ParameterNode(value: final bv, sqlType: final bt), + ) => + av == bv && at == bt, + ( + FunctionNode( + name: final an, + arguments: final aa, + distinct: final ad, + decimal: final ax, + ), + FunctionNode( + name: final bn, + arguments: final ba, + distinct: final bd, + decimal: final bx, + ), + ) => + an == bn && + ax == bx && + ad == bd && + aa.length == ba.length && + aa.indexed.every((e) => sameSqlNode(e.$2, ba[e.$1])), + ( + BinaryNode(left: final al, op: final ao, right: final ar), + BinaryNode(left: final bl, op: final bo, right: final br), + ) => + ao == bo && sameSqlNode(al, bl) && sameSqlNode(ar, br), + _ => false, + }; +} diff --git a/lib/src/mutation.dart b/lib/src/query/mutation.dart similarity index 51% rename from lib/src/mutation.dart rename to lib/src/query/mutation.dart index 3ee5f388..80dfcca3 100644 --- a/lib/src/mutation.dart +++ b/lib/src/query/mutation.dart @@ -1,62 +1,120 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; +import '../../driver.dart'; +import 'context.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; + +/// One typed column assignment prepared by a writable field. +/// +/// Obtain assignments through a field's `set`, `setExpression`, `increment`, or +/// `defaultValue` methods, then return them from an insert or update callback. final class Assignment { + /// Writable field belonging to the mutation's table occurrence. final Field field; - final _Node? _value; - const Assignment._(this.field, this._value); + + /// @nodoc + @internal + final SqlNode? assignedValue; + + /// @nodoc + @internal + const Assignment.internal(this.field, this.assignedValue); } -enum _MutationKind { insert, update, delete } +/// @nodoc +@internal +enum MutationKind { insert, update, delete } +/// A prepared insert, update, or delete that performs no I/O until executed. +/// +/// [compile] inspects SQL; [execute] returns the affected-row count; +/// [returning] prepares a typed result. Updates and deletes accept table filters, +/// not joins, ordering, grouping, or pagination. Client defaults in a prepared +/// insert are evaluated when it is built and reused if it executes again. final class Mutation { + /// Query context that owns this mutation's execution and transaction scope. final QueryContext database; - final F _fields; - final _QueryState _state; - final _MutationKind _kind; - final List _assignments; - final F Function(TableRef)? _createFields; + + /// @nodoc + @internal + final F queryFields; + + /// @nodoc + @internal + final QueryState queryState; + + /// @nodoc + @internal + final MutationKind mutationKind; + + /// @nodoc + @internal + final List writeAssignments; + + /// @nodoc + @internal + final F Function(TableRef)? fieldsFactory; final _Conflict? _conflict; - final List>? _rows; - Mutation._( + + /// @nodoc + @internal + final List>? insertRows; + + /// @nodoc + @internal + Mutation.internal( this.database, - this._fields, - this._state, - this._kind, + this.queryFields, + this.queryState, + this.mutationKind, List assignments, { - this._createFields, + this.fieldsFactory, this._conflict, - this._rows, - }) : _assignments = List.unmodifiable(assignments); + this.insertRows, + }) : writeAssignments = List.unmodifiable(assignments); + /// Prepares SQLite/PostgreSQL duplicate-key suppression for this insert. + /// + /// An explicit [target] must match a declared primary or unique key. Omitting + /// it uses the database's untargeted `ON CONFLICT DO NOTHING` behavior. Mutation onConflictDoNothing({ List> Function(F)? target, - }) => _withConflict(target == null ? [] : target(_fields), const [], null); + }) => + _withConflict(target == null ? [] : target(queryFields), const [], null); /// MySQL/MariaDB update on any duplicate unique key, matching native semantics. + /// + /// [set] receives the existing row and the proposed incoming row. It must + /// return at least one assignment; this method does not execute the insert. Mutation onDuplicateKeyUpdate({ required List Function(F existing, F incoming) set, }) { - if (_kind != _MutationKind.insert || _createFields == null) { + final fieldsFactory = this.fieldsFactory; + if (mutationKind != MutationKind.insert || fieldsFactory == null) { throw const OrmException( 'MUTATION.CONFLICT', 'Upsert applies to an insert.', ); } - final incoming = _createFields(TableRef(_state.source.schema)); - final assignments = set(_fields, incoming); + final incoming = fieldsFactory(TableRef(queryState.source.schema)); + final assignments = set(queryFields, incoming); if (assignments.isEmpty) { throw const OrmException( 'MUTATION.EMPTY', 'Conflict update needs assignments.', ); } - return Mutation._( + return Mutation.internal( database, - _fields, - _state, - _kind, - _assignments, - createFields: _createFields, + queryFields, + queryState, + mutationKind, + writeAssignments, + fieldsFactory: fieldsFactory, conflict: _Conflict( const [], List.unmodifiable(assignments), @@ -65,25 +123,30 @@ final class Mutation { ); } + /// Prepares a SQLite/PostgreSQL upsert against a declared unique [target]. + /// + /// [set] distinguishes existing values from proposed incoming values and must + /// return at least one assignment. MySQL/MariaDB use [onDuplicateKeyUpdate]. Mutation onConflictUpdate({ required List> Function(F) target, required List Function(F existing, F incoming) set, }) { - if (_createFields == null) { + final fieldsFactory = this.fieldsFactory; + if (fieldsFactory == null) { throw const OrmException( 'MUTATION.CONFLICT', 'Upsert applies to an insert.', ); } - final incoming = _createFields(TableRef(_state.source.schema)); - final assignments = set(_fields, incoming); + final incoming = fieldsFactory(TableRef(queryState.source.schema)); + final assignments = set(queryFields, incoming); if (assignments.isEmpty) { throw const OrmException( 'MUTATION.EMPTY', 'Conflict update needs assignments.', ); } - return _withConflict(target(_fields), assignments, incoming.table); + return _withConflict(target(queryFields), assignments, incoming.table); } Mutation _withConflict( @@ -91,13 +154,13 @@ final class Mutation { List assignments, TableRef? incoming, ) { - if (_kind != _MutationKind.insert) { + if (mutationKind != MutationKind.insert) { throw const OrmException( 'MUTATION.CONFLICT', 'Upsert applies to an insert.', ); } - final schema = _state.source.schema; + final schema = queryState.source.schema; final names = [for (final field in target) field.definition.name]; if (names.isEmpty && assignments.isNotEmpty) { throw const OrmException( @@ -105,7 +168,7 @@ final class Mutation { 'Conflict update requires a unique key target.', ); } - if (target.any((f) => f.table != _state.source)) { + if (target.any((f) => f.table != queryState.source)) { throw const OrmException( 'QUERY.SCOPE', 'Conflict target belongs to another table.', @@ -128,13 +191,13 @@ final class Mutation { 'Conflict target must be a declared unique key.', ); } - return Mutation._( + return Mutation.internal( database, - _fields, - _state, - _kind, - _assignments, - createFields: _createFields, + queryFields, + queryState, + mutationKind, + writeAssignments, + fieldsFactory: fieldsFactory, conflict: _Conflict( List.unmodifiable(names), List.unmodifiable(assignments), @@ -143,7 +206,9 @@ final class Mutation { ); } - SqlCommand _compile([_SelectionPlan? selection]) { + /// @nodoc + @internal + SqlCommand compileQuery([SelectionPlan? selection]) { if (selection != null && selection.columns.isEmpty) { throw const OrmException( 'QUERY.EMPTY_SELECTION', @@ -151,7 +216,9 @@ final class Mutation { ); } if (selection != null && - selection.columns.any((e) => _aggregate(e._node) || _window(e._node))) { + selection.columns.any( + (e) => aggregate(e.expressionNode) || window(e.expressionNode), + )) { throw const OrmException( 'MUTATION.RETURNING', 'RETURNING cannot contain aggregate or window functions.', @@ -164,23 +231,23 @@ final class Mutation { 'RETURNING selects scalar fields; query relations after the mutation.', ); } - if (_state.limit != null || - _state.joins.isNotEmpty || - _state.union != null || - _state.ctes.isNotEmpty || - _state.offset != null || - _state.order.isNotEmpty || - _state.group.isNotEmpty || - _state.having != null || - _state.distinct) { + if (queryState.limit != null || + queryState.joins.isNotEmpty || + queryState.union != null || + queryState.ctes.isNotEmpty || + queryState.offset != null || + queryState.order.isNotEmpty || + queryState.group.isNotEmpty || + queryState.having != null || + queryState.distinct) { throw const OrmException( 'MUTATION.QUERY', 'Mutations accept a table and WHERE; select keys for paginated mutations.', ); } - final w = _Writer( + final w = SqlWriter( database.dialect, - {_state.source: 't0'}, + {queryState.source: 't0'}, database: database, exactDecimal: database.capabilities.exactDecimal, temporal: database.capabilities.temporal, @@ -188,15 +255,15 @@ final class Mutation { void validate(List assignments) { final names = {}; for (final a in assignments) { - if (a._value case final node?) { - if (_aggregate(node) || _window(node)) { + if (a.assignedValue case final node?) { + if (aggregate(node) || window(node)) { throw const OrmException( 'QUERY.AGGREGATE', 'Assignments cannot contain aggregate or window functions. Use a scalar subquery.', ); } } - if (a._value == null && + if (a.assignedValue == null && !a.field.definition.generated && a.field.definition.defaultSql == null) { throw const OrmException( @@ -204,7 +271,7 @@ final class Mutation { 'Column has no declared database default.', ); } - if (a.field.table != _state.source) { + if (a.field.table != queryState.source) { throw const OrmException( 'QUERY.SCOPE', 'Assignment belongs to another table.', @@ -219,21 +286,22 @@ final class Mutation { } } - validate(_assignments); - if (_state.predicate case final predicate?) { - if (_aggregate(predicate._node) || _window(predicate._node)) { + validate(writeAssignments); + if (queryState.predicate case final predicate?) { + if (aggregate(predicate.expressionNode) || + window(predicate.expressionNode)) { throw const OrmException( 'QUERY.AGGREGATE', 'Mutation predicates cannot contain aggregate or window functions. Use a subquery.', ); } } - for (final row in _rows ?? >[]) { + for (final row in insertRows ?? >[]) { validate(row); } if (_conflict case final conflict?) validate(conflict.assignments); String assigned(Assignment a) { - if (a._value case final expression?) return expression.write(w); + if (a.assignedValue case final expression?) return expression.write(w); if (a.field.definition.defaultSql == null) { throw const OrmException( 'MUTATION.DEFAULT', @@ -249,16 +317,18 @@ final class Mutation { return 'DEFAULT'; } - final table = w.mysql && _kind == _MutationKind.insert - ? w.quote(_state.source.schema.name) - : '${w.quote(_state.source.schema.name)} AS ${w.quote('t0')}'; - if (w.mysql && _kind == _MutationKind.insert) { - w.unqualifiedTable = _state.source; + final table = w.mysql && mutationKind == MutationKind.insert + ? w.quote(queryState.source.schema.name) + : '${w.quote(queryState.source.schema.name)} AS ${w.quote('t0')}'; + if (w.mysql && mutationKind == MutationKind.insert) { + w.unqualifiedTable = queryState.source; } final b = StringBuffer(); - switch (_kind) { - case _MutationKind.insert: - final values = _assignments.where((a) => a._value != null).toList(); + switch (mutationKind) { + case MutationKind.insert: + final values = writeAssignments + .where((a) => a.assignedValue != null) + .toList(); b.write('INSERT INTO $table'); if (values.isEmpty) { b.write(w.mysql ? ' () VALUES ()' : ' DEFAULT VALUES'); @@ -266,26 +336,26 @@ final class Mutation { b.write( ' (${values.map((a) => w.quote(a.field.definition.name)).join(', ')})', ); - final rows = _rows ?? [_assignments]; + final rows = insertRows ?? [writeAssignments]; b.write( - ' VALUES ${rows.map((row) => '(${row.where((a) => a._value != null).map(assigned).join(', ')})').join(', ')}', + ' VALUES ${rows.map((row) => '(${row.where((a) => a.assignedValue != null).map(assigned).join(', ')})').join(', ')}', ); } - case _MutationKind.update: - if (_assignments.isEmpty) { + case MutationKind.update: + if (writeAssignments.isEmpty) { throw const OrmException( 'MUTATION.EMPTY', 'Update needs an assignment.', ); } b.write( - 'UPDATE $table SET ${_assignments.map((a) => '${w.quote(a.field.definition.name)} = ${assigned(a)}').join(', ')}', + 'UPDATE $table SET ${writeAssignments.map((a) => '${w.quote(a.field.definition.name)} = ${assigned(a)}').join(', ')}', ); - case _MutationKind.delete: + case MutationKind.delete: b.write('DELETE FROM $table'); } - if (_state.predicate case final predicate?) { - b.write(' WHERE ${predicate._node.write(w)}'); + if (queryState.predicate case final predicate?) { + b.write(' WHERE ${predicate.expressionNode.write(w)}'); } if (_conflict case final conflict?) { if (w.mysql) { @@ -343,42 +413,68 @@ final class Mutation { } w.unqualified = database.dialect == SqlDialect.sqlite; b.write( - ' RETURNING ${selection.columns.map((e) => e._node.write(w)).join(', ')}', + ' RETURNING ${selection.columns.map((e) => e.expressionNode.write(w)).join(', ')}', ); } return w.finish(b.toString()); } - SqlCommand compile() => _compile(); + /// Validates and compiles SQL with separate parameters, without database I/O. + SqlCommand compile() => compileQuery(); + + /// Executes the complete mutation and returns the driver's affected-row count. + /// + /// Counts follow the selected database's native semantics, including upserts. Future execute({ ExecutionOptions options = const ExecutionOptions(), }) async => (await database.executeCommand( compile(), options: options, - changedTables: [_state.source.schema], + changedTables: [queryState.source.schema], affectedOnly: true, - cascade: _kind == _MutationKind.delete, + cascade: mutationKind == MutationKind.delete, )).affectedRows; + + /// Prepares a typed `RETURNING` result on an engine that supports it. + /// + /// Select scalar SQL expressions or composed projections. Relationships, + /// aggregate functions, and window functions are rejected before execution. Returning returning(Selection Function(F) selection) => - Returning._(this, selection(_fields)); + Returning.internal(this, selection(queryFields)); } +/// A prepared mutation whose returned SQL rows decode to `R`. +/// +/// Every read method executes the entire mutation again. Selecting only its +/// first result does not limit affected rows. Cardinality and decoding errors +/// can occur after the write; let them escape a transaction callback when that +/// transaction must roll back the changes. final class Returning { final Mutation _mutation; - final Selection _selection; - Returning._(this._mutation, this._selection); + + /// @nodoc + @internal + final Selection querySelection; + + /// @nodoc + @internal + Returning.internal(this._mutation, this.querySelection); + + /// Executes the mutation and decodes all rows from `RETURNING`. + /// + /// Returned row order is determined by the database and has no implicit sort. Future> get({ ExecutionOptions options = const ExecutionOptions(), }) async { - final plan = _SelectionPlan(); - final decode = _selection._bind(plan); - final command = _mutation._compile(plan); + final plan = SelectionPlan(); + final decode = querySelection.bindSelection(plan); + final command = _mutation.compileQuery(plan); final result = await _mutation.database.executeCommand( command, options: options, - changedTables: [_mutation._state.source.schema], + changedTables: [_mutation.queryState.source.schema], affectedOnly: true, - cascade: _mutation._kind == _MutationKind.delete, + cascade: _mutation.mutationKind == MutationKind.delete, ); return _mutation.database.observeDecode( command.sql, @@ -387,6 +483,10 @@ final class Returning { ); } + /// Executes the mutation and requires exactly one returned row. + /// + /// Throws `QUERY.CARDINALITY` after execution if the result is empty or has + /// multiple rows. Use a transaction if that failure must roll back the write. Future single({ ExecutionOptions options = const ExecutionOptions(), }) async { @@ -400,9 +500,44 @@ final class Returning { return result.single; } - Future first({ + /// Executes the mutation and returns its first row, rejecting an empty result. + /// + /// The mutation still affects all matching rows. Cardinality is checked after + /// execution; a returned SQL NULL is a row when `R` is nullable. + Future first({ExecutionOptions options = const ExecutionOptions()}) async { + final rows = await get(options: options); + if (rows.isEmpty) { + throw const OrmException( + 'QUERY.CARDINALITY', + 'Expected a returned row, received none.', + ); + } + return rows.first; + } + + /// Executes the mutation and returns the first returned row, or null if empty. + /// + /// All matching rows are affected, regardless of how many are returned here. + Future firstOrNull({ ExecutionOptions options = const ExecutionOptions(), }) async => (await get(options: options)).firstOrNull; + + /// Executes the mutation and accepts zero or one returned row. + /// + /// Throws `QUERY.CARDINALITY` after execution for multiple rows. Use a + /// transaction if that failure must roll back the write. + Future singleOrNull({ + ExecutionOptions options = const ExecutionOptions(), + }) async { + final rows = await get(options: options); + if (rows.length > 1) { + throw OrmException( + 'QUERY.CARDINALITY', + 'Expected at most one returned row, received ${rows.length}.', + ); + } + return rows.firstOrNull; + } } final class _Conflict( diff --git a/lib/src/named_sql.dart b/lib/src/query/named_sql.dart similarity index 79% rename from lib/src/named_sql.dart rename to lib/src/query/named_sql.dart index 2342f9c0..f799c10b 100644 --- a/lib/src/named_sql.dart +++ b/lib/src/query/named_sql.dart @@ -1,14 +1,34 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import 'context.dart'; +import 'cte.dart'; +import 'expression.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'table.dart'; /// Fixed SQL with :named bound values. Quoted text, identifiers and comments are /// preserved. This tokenizer is not a SQL parser; use database query checks. final class SqlTemplate { + /// Database engine whose quoting and parameter rules this template uses. final SqlDialect dialect; final List _parts; + + /// Immutable names of all required bound parameters, excluding repeated uses. final Set parameters; - SqlTemplate._(this.dialect, this._parts, this.parameters); + + /// @nodoc + @internal + SqlTemplate.internal(this.dialect, this._parts, this.parameters); + + /// Tokenizes a trusted SELECT, WITH, or VALUES source without database access. + /// + /// Named placeholders bind values; they do not substitute identifiers or + /// arbitrary SQL. Validate SQL and result metadata against the target database + /// before shipping a generated query. factory SqlTemplate(String source, {required SqlDialect dialect}) { - _checkSqlText(source); + checkSqlText(source); final mysql = dialect == SqlDialect.mysql || dialect == SqlDialect.mariadb; final parts = [], names = {}; final text = StringBuffer(); @@ -153,24 +173,28 @@ final class SqlTemplate { fail('A named query requires SELECT, WITH or VALUES'); } parts.add(text.toString()); - return SqlTemplate._( + return SqlTemplate.internal( dialect, List.unmodifiable(parts), Set.unmodifiable(names), ); } + /// Compiles SQL and bound parameters without executing the template. + /// + /// [arguments] must contain exactly [parameters], each represented by a bound + /// value expression. Dialect mismatches and parameter overflow are rejected. SqlCommand compile( Capabilities capabilities, Map> arguments, ) { - final w = _Writer( + final w = SqlWriter( capabilities.dialect, {}, exactDecimal: capabilities.exactDecimal, temporal: capabilities.temporal, ); - final sql = _write(w, arguments); + final sql = writeQuery(w, arguments); final command = w.finish(sql); if (command.parameters.length > capabilities.maxParameters) { throw const OrmException( @@ -181,7 +205,9 @@ final class SqlTemplate { return command; } - String _write(_Writer w, Map> arguments) { + /// @nodoc + @internal + String writeQuery(SqlWriter w, Map> arguments) { if (w.dialect != dialect) { throw const OrmException( 'QUERY.DIALECT', @@ -202,13 +228,13 @@ final class SqlTemplate { String() => part, _SqlSlot(:final name) => bound.putIfAbsent(name, () { final expression = arguments[name]!; - if (_unwrapStorage(expression._node) is! _Parameter) { + if (unwrapStorage(expression.expressionNode) is! ParameterNode) { throw const OrmException( 'SQL.PARAMETERS', 'Named SQL arguments must be bound values.', ); } - return expression._node.write(w); + return expression.expressionNode.write(w); }), _ => throw StateError('Unknown SQL fragment'), }, @@ -222,10 +248,21 @@ final class _SqlSlot(final String name); /// Generated query definitions reuse table fields and the ordinary Query /// compiler. Their CTE source makes table mutations invalid before execution. final class SqlQueryDefinition { + /// Declared result columns, codecs, and row decoder. final Table result; + + /// Immutable SQL variants keyed by their target database engine. final Map sql; + + /// Combines a typed result declaration with trusted engine-specific SQL. SqlQueryDefinition(this.result, Map sql) : sql = Map.unmodifiable(sql); + + /// Creates a query bound to [db], without executing its SQL source. + /// + /// The context selects its engine's template. Further filters and projections + /// operate on the declared result columns; generated bindings normally supply + /// the typed argument map. No fallback to another engine's SQL is attempted. Query bind(QueryContext db, Map> arguments) { final template = sql[db.dialect]; if (template == null) { @@ -235,10 +272,10 @@ final class SqlQueryDefinition { ); } final fields = result.createFields(TableRef(result.schema)); - return Query._( + return Query.internal( db, fields, - _QueryState( + QueryState( fields.table, ctes: [ _SqlCte(result.schema.name, template, Map.unmodifiable(arguments)), @@ -253,10 +290,12 @@ final class _SqlCte( @override final String name, final SqlTemplate template, final Map> arguments, -) implements _CteDefinition { +) implements CteDefinition { + /// @nodoc + @internal @override - String _writeDefinition(_Writer writer) { + String writeDefinition(SqlWriter writer) { if (writer.reads case final reads?) reads.opaque = true; - return '${writer.quote(name)} AS (\n${template._write(writer, arguments)}\n)'; + return '${writer.quote(name)} AS (\n${template.writeQuery(writer, arguments)}\n)'; } } diff --git a/lib/src/query/nodes.dart b/lib/src/query/nodes.dart new file mode 100644 index 00000000..741a5bcf --- /dev/null +++ b/lib/src/query/nodes.dart @@ -0,0 +1,817 @@ +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import 'context.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'query.dart'; +import 'reads.dart'; +import 'relation.dart'; +import 'table.dart'; + +/// @nodoc +@internal +sealed class SqlNode { + const SqlNode(); + String write(SqlWriter w) => w.project?.call(this) ?? writeSql(w); + String writeSql(SqlWriter w); +} + +/// @nodoc +@internal +SqlNode unwrapStorage(SqlNode node) => switch (node) { + DecimalNode(:final child) || TemporalNode(:final child) => child, + _ => node, +}; + +/// @nodoc +@internal +final class ColumnNode(final TableRef table, final String name) + extends SqlNode { + @override + String writeSql(SqlWriter w) { + final alias = w.aliases[table]; + if (alias == null) { + throw const OrmException( + 'QUERY.SCOPE', + 'Column belongs to another query.', + ); + } + if (w.mysql && alias == 'excluded') return 'VALUES(${w.quote(name)})'; + return w.unqualified || w.unqualifiedTable == table + ? w.quote(name) + : '${w.quote(alias)}.${w.quote(name)}'; + } +} + +/// @nodoc +@internal +final class ParameterNode( + final Object? value, { + final String? sqlType, + final String? storageType, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) { + final parameter = w.parameter(value, storageType: storageType ?? sqlType); + if (w.mysql && (storageType ?? sqlType) == 'json') { + return w.dialect == SqlDialect.mysql + ? 'CAST($parameter AS JSON)' + : "JSON_EXTRACT($parameter, '\$')"; + } + if (w.mysql && + {'decimal', 'bigint'}.contains(storageType ?? sqlType) && + value != null) { + final decimal = Decimal.parse(value as String); + final text = decimal.toString(); + final digits = text.replaceAll('-', '').split('.'); + final scale = digits.length == 2 ? digits[1].length : 0; + final precision = digits[0].length + scale; + if (precision > 65 || scale > 30) { + throw const OrmException( + 'CAPABILITY.DECIMAL', + 'MySQL/MariaDB SQL decimals require at most 65 digits and scale 30.', + ); + } + return 'CAST($parameter AS DECIMAL(65,$scale))'; + } + // A standalone SELECT parameter has no column context in PostgreSQL and + // otherwise resolves to text, including inside a UNION operand. + if (sqlType == null || w.dialect != SqlDialect.postgres) return parameter; + final type = switch (sqlType) { + 'integer' => 'BIGINT', + 'bigint' || 'decimal' => 'NUMERIC', + 'text' => 'TEXT', + 'real' => 'DOUBLE PRECISION', + 'boolean' => 'BOOLEAN', + 'timestamp' || 'instant' => 'TIMESTAMPTZ', + 'date' => 'DATE', + 'time' => 'TIME WITHOUT TIME ZONE', + 'local_datetime' => 'TIMESTAMP WITHOUT TIME ZONE', + 'json' => 'JSONB', + 'blob' => 'BYTEA', + _ => throw OrmException( + 'QUERY.PARAMETER_TYPE', + 'No PostgreSQL parameter type for $sqlType.', + ), + }; + return 'CAST($parameter AS $type)'; + } +} + +/// @nodoc +@internal +final class BinaryNode(final SqlNode left, final String op, final SqlNode right) + extends SqlNode { + @override + String writeSql(SqlWriter w) => '(${left.write(w)} $op ${right.write(w)})'; +} + +/// @nodoc +@internal +final class UnaryNode( + final String op, + final SqlNode child, { + final bool postfix = false, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) => + postfix ? '(${child.write(w)} $op)' : '($op ${child.write(w)})'; +} + +/// @nodoc +@internal +final class FunctionNode( + final String name, + final List arguments, { + final bool distinct = false, + final bool decimal = false, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (decimal && w.mysql && name == 'SUM') { + throw const OrmException( + 'CAPABILITY.DECIMAL_PRECISION', + 'MySQL/MariaDB decimal sums can silently saturate in subqueries; use explicit native SQL when its precision is acceptable.', + ); + } + return '${decimal && w.dialect == SqlDialect.sqlite ? 'orm_decimal_${name.toLowerCase()}_v1' : name}(${distinct ? 'DISTINCT ' : ''}' + '${arguments.map((e) => e.write(w)).join(', ')})'; + } +} + +/// @nodoc +@internal +final class InNode(final SqlNode expression, final List values) + extends SqlNode { + @override + String writeSql(SqlWriter w) => values.isEmpty + ? 'FALSE' + : '(${expression.write(w)} IN (${values.map((v) => v.write(w)).join(', ')}))'; +} + +/// @nodoc +@internal +/// Trusted SQL fragments still bind embedded expressions as parameters or ASTs. +final class RawNode(final List parts, final List values) + extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (w.reads?.includeRaw == true) w.reads!.opaque = true; + final result = StringBuffer(parts.first); + for (var i = 0; i < values.length; i++) { + result + ..write(values[i].write(w)) + ..write(parts[i + 1]); + } + return result.toString(); + } +} + +/// @nodoc +@internal +final class SqlWriter { + final SqlDialect dialect; + final QueryContext? database; + final Map aliases; + final List parameters = []; + final Set leftJoins = {}; + final ReadTables? reads; + final bool exactDecimal; + final bool temporal; + bool unqualified = false; + TableRef? unqualifiedTable; + String? Function(SqlNode)? project; + AverageInputs? averageInputs; + SqlWriter( + this.dialect, + this.aliases, { + this.database, + this.reads, + this.exactDecimal = false, + this.temporal = false, + }); + bool get mysql => + dialect == SqlDialect.mysql || dialect == SqlDialect.mariadb; + String quote(String name) { + checkSqlText(name); + return mysql + ? '`${name.replaceAll('`', '``')}`' + : '"${name.replaceAll('"', '""')}"'; + } + + String parameter(Object? value, {String? storageType}) { + // Codec storage, not string pattern guessing, determines temporal binding. + if (mysql && value is String && storageType == 'instant') { + final instant = Codecs.dateTime.decode(value).toUtc(); + if (instant.year < 1000 || instant.year > 9999) { + throw const OrmException( + 'CAPABILITY.TEMPORAL', + 'MySQL timestamps require years 1000 through 9999.', + ); + } + value = value.substring(0, value.length - 3); + } + parameters.add(switch ((dialect, value)) { + (SqlDialect.sqlite, bool v) => v ? 1 : 0, + (SqlDialect.sqlite, DateTime v) => v.toUtc().toIso8601String(), + _ => value, + }); + return dialect == SqlDialect.postgres + ? '\$${parameters.length}' + : mysql + ? '\u0001${parameters.length}\u0002' + : '?${parameters.length}'; + } + + SqlCommand finish(String sql) { + SqlCommand checked(String text, List values) { + if (database != null && + values.length > database!.capabilities.maxParameters) { + throw const OrmException( + 'QUERY.PARAMETERS', + 'SQL exceeds the parameter limit.', + ); + } + return SqlCommand(text, values); + } + + if (!mysql) return checked(sql, parameters); + // Construction may render ORDER BY before WHERE, and window staging may + // reuse a fragment. Positional protocols bind in final SQL occurrence order. + final ordered = []; + final text = sql.replaceAllMapped(RegExp('\u0001([0-9]+)\u0002'), (match) { + ordered.add(parameters[int.parse(match[1]!) - 1]); + return '?'; + }); + return checked(text, ordered); + } +} + +/// @nodoc +@internal +void checkSqlText(String text) { + if (text.contains('\u0001') || text.contains('\u0002')) { + throw const OrmException( + 'SQL.TEXT', + 'SQL text contains a reserved control character.', + ); + } +} + +/// @nodoc +@internal +final class RelationSubqueryNode( + final Relation relation, + final bool count, +) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (relation.queryState.limit != null || + relation.queryState.offset != null) { + throw const OrmException( + 'RELATION.AGGREGATE', + 'Apply relation count/any/every before pagination.', + ); + } + final source = relation.queryState.source; + w.reads?.tables.add(source.schema); + final previous = w.aliases[source]; + final alias = 't${w.aliases.length}'; + w.aliases[source] = alias; + try { + final predicates = [ + for (var i = 0; i < relation.childFields.length; i++) + BinaryNode( + relation.childFields[i].expressionNode, + '=', + relation.parentFields[i].expressionNode, + ).write(w), + if (relation.queryState.predicate case final p?) + p.expressionNode.write(w), + ]; + final query = + 'SELECT ${count ? 'COUNT(*)' : '1'} FROM ${w.quote(source.schema.name)} AS ${w.quote(alias)} WHERE ${predicates.join(' AND ')}'; + return count ? '($query)' : 'EXISTS ($query)'; + } finally { + if (previous == null) { + w.aliases.remove(source); + } else { + w.aliases[source] = previous; + } + } + } +} + +// Row-value IN avoids a deep OR tree for large composite-key batches. +/// @nodoc +@internal +final class RelationKeys( + final List> columns, + final List keys, +) extends SqlNode { + @override + String writeSql(SqlWriter w) { + // Keys stay comparable storage values while grouping. Restore floating SQL + // intent only when binding them (JS cannot identify an integral double). + Object? parameter(int index, Object? value) => + value != null && columns[index].codec.sqlType == 'real' + ? Codecs.real.encode(Codecs.real.decode(value)) + : value; + if (columns.length == 1) { + return InNode(columns.single.expressionNode, [ + for (final key in keys) + ParameterNode( + parameter(0, key.values.single), + storageType: columns.single.codec.sqlType, + ), + ]).write(w); + } + return '(${columns.map((c) => c.expressionNode.write(w)).join(', ')}) IN (${w.dialect == SqlDialect.sqlite ? 'VALUES ' : ''}' + '${keys.map((key) => '(${[for (var i = 0; i < key.values.length; i++) ParameterNode(parameter(i, key.values[i]), storageType: columns[i].codec.sqlType).write(w)].join(', ')})').join(', ')})'; + } +} + +/// @nodoc +@internal +final class PresenceNode(final TableAlias alias) + extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (!w.leftJoins.contains(alias.fields.table)) { + throw const OrmException( + 'QUERY.OUTER_JOIN', + 'Optional projections require this alias to be left joined.', + ); + } + return '${w.quote(w.aliases[alias.fields.table]!)}.${w.quote(alias.presenceMarker)}'; + } +} + +/// @nodoc +@internal +final class SubqueryNode( + final Query query, { + final bool exists = false, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) { + final (plan, _) = query.planQuery(); + if (plan.relations.isNotEmpty) { + throw const OrmException( + 'QUERY.SUBQUERY', + 'SQL subqueries cannot include batch-loaded relations.', + ); + } + if (w.dialect != query.database.dialect) { + throw const OrmException( + 'QUERY.DIALECT', + 'Subqueries must use the enclosing SQL dialect.', + ); + } + return '${exists ? 'EXISTS ' : ''}(${query.writeQuery(w, plan)})'; + } +} + +/// @nodoc +@internal +final class WindowNode( + final SqlNode function, + final List> partition, + final List order, + final WindowFrame? frame, +) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (function case final DecimalAverage average) { + return average.writeAverage(w, window: this); + } + final clauses = []; + if (partition.isNotEmpty) { + clauses.add( + 'PARTITION BY ${partition.map((e) => e.expressionNode.write(w)).join(', ')}', + ); + } + if (order.isNotEmpty) { + clauses.add('ORDER BY ${order.map((o) => o.writeQuery(w)).join(', ')}'); + } + if (frame != null) { + clauses.add(switch (frame!) { + WindowFrame.rowsToCurrent => + 'ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW', + WindowFrame.rowsAll => + 'ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING', + WindowFrame.rangeToCurrent => + 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW', + }); + } + return '${function.writeSql(w)} OVER (${clauses.join(' ')})'; + } +} + +/// @nodoc +@internal +final class TemporalNode(final SqlNode child, final String kind) + extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (!w.temporal) { + throw const OrmException( + 'CAPABILITY.TEMPORAL', + 'This driver does not provide temporal values.', + ); + } + final text = child.write(w); + return w.dialect == SqlDialect.sqlite + ? '($text COLLATE "orm_${kind}_v1")' + : text; + } +} + +/// @nodoc +@internal +final class TemporalCast( + final SqlNode child, + final String kind, + final int digits, { + final bool columnAssignment = false, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) { + checkTemporalPrecision(digits); + if (!w.temporal) { + throw const OrmException( + 'CAPABILITY.TEMPORAL', + 'This driver does not provide temporal values.', + ); + } + final type = switch (kind) { + 'time' => 'TIME($digits) WITHOUT TIME ZONE', + 'local_datetime' => 'TIMESTAMP($digits) WITHOUT TIME ZONE', + 'instant' => 'TIMESTAMPTZ($digits)', + _ => throw ArgumentError('Temporal precision requires temporal storage.'), + }; + final sql = child.write(w); + if (w.mysql) { + // Storage coercion follows the declared database column. Expression + // rounding has a stronger cross-engine contract and stays explicit. + if (columnAssignment) return sql; + throw const OrmException( + 'CAPABILITY.TEMPORAL_PRECISION', + 'MySQL/MariaDB temporal rounding differs; declare column precision or use explicit SQL.', + ); + } + return w.dialect == SqlDialect.postgres + ? 'CAST($sql AS $type)' + : "orm_temporal_cast_v1($sql, '$kind', $digits)"; + } +} + +// Attach collation to every decimal expression, including computed projections, +// CTE references and UNION outputs. Native PostgreSQL NUMERIC needs no wrapper. +/// @nodoc +@internal +final class DecimalNode(final SqlNode child) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (!w.exactDecimal) { + throw const OrmException( + 'CAPABILITY.DECIMAL', + 'This driver does not provide exact decimal SQL.', + ); + } + final sql = child.write(w); + return w.dialect == SqlDialect.sqlite + ? '($sql COLLATE "orm_decimal_v1")' + : sql; + } +} + +/// @nodoc +@internal +final class DecimalCast( + final SqlNode child, + final int precision, + final int scale, { + final bool columnAssignment = false, +}) extends SqlNode { + @override + String writeSql(SqlWriter w) { + Decimal.validateDigits(precision, scale); + if (!w.exactDecimal) { + throw const OrmException( + 'CAPABILITY.DECIMAL', + 'This driver does not provide exact decimal SQL.', + ); + } + final expression = child.write(w); + if (w.mysql) { + if (precision > 65 || scale < 0 || scale > 30 || scale > precision) { + throw const OrmException( + 'CAPABILITY.DECIMAL', + 'MySQL/MariaDB decimals require precision 1–65 and scale 0–min(precision, 30).', + ); + } + if (columnAssignment) return expression; + throw const OrmException( + 'CAPABILITY.DECIMAL_PRECISION', + 'MySQL/MariaDB decimal casts may clamp overflow; use explicit native SQL when its precision is acceptable.', + ); + } + return w.dialect == SqlDialect.postgres + ? 'CAST($expression AS NUMERIC($precision,$scale))' + : 'orm_decimal_cast_v1($expression, $precision, $scale)'; + } +} + +/// @nodoc +@internal +final class DecimalArithmetic( + final SqlNode left, + final String op, + final SqlNode right, +) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (w.mysql) { + throw const OrmException( + 'CAPABILITY.DECIMAL_PRECISION', + 'MySQL/MariaDB arithmetic may silently truncate decimal digits; use explicit native SQL when its precision is acceptable.', + ); + } + final a = left.write(w), b = right.write(w); + if (w.dialect != SqlDialect.sqlite) return '($a $op $b)'; + final name = switch (op) { + '+' => 'add', + '-' => 'sub', + '*' => 'mul', + _ => throw StateError('Invalid decimal operation'), + }; + return 'orm_decimal_${name}_v1($a, $b)'; + } +} + +/// @nodoc +@internal +/// A null divisor denotes rounding one value. Operands are evaluated once. +final class DecimalRatio( + final SqlNode numerator, + final SqlNode? divisor, + final int scale, + final DecimalRounding rounding, +) extends SqlNode { + @override + String writeSql(SqlWriter w) { + if (w.mysql) { + throw const OrmException( + 'CAPABILITY.DECIMAL_ROUNDING', + 'Exact SQL division and rounding are not implemented for MySQL/MariaDB; use an explicit DECIMAL expression.', + ); + } + final a = numerator.write(w), b = divisor?.write(w); + if (w.dialect == SqlDialect.sqlite) { + return b == null + ? 'orm_decimal_round_v1($a, $scale, ${rounding.index})' + : 'orm_decimal_div_v1($a, $b, $scale, ${rounding.index})'; + } + // MATERIALIZED keeps volatile expressions and correlated operands from + // being reevaluated by the quotient, remainder and rounding calculations. + final prefix = + ''' +WITH orm_decimal_args(a, b) AS MATERIALIZED ( + VALUES (CAST($a AS NUMERIC), CAST(${b ?? '1'} AS NUMERIC)) +), orm_decimal_parts AS MATERIALIZED ( + SELECT abs(a) AS a, abs(b) AS b, sign(a) * sign(b) AS sgn, + a IS NULL OR b IS NULL AS nil, + div(abs(a), NULLIF(abs(b), 0)) AS q + FROM orm_decimal_args +), orm_decimal_remainder AS MATERIALIZED ( + SELECT *, a - q * b AS r FROM orm_decimal_parts +)'''; + return _DecimalRoundSql(scale, rounding).write(prefix); + } +} + +// The input CTE exposes a nonnegative integer quotient q, remainder r < b, +// positive denominator b, result sign sgn and empty/null marker nil. +final class _DecimalRoundSql(final int scale, final DecimalRounding rounding) { + String write(String prefix) => + scale >= 0 ? _positive(prefix) : _negative(prefix); + + String _positive(String prefix) { + final factor = "NUMERIC '1e$scale'"; + final unit = "NUMERIC '1e-$scale'"; + final text = scale == 0 + ? 'q::text' + : "q::text || '.' || lpad(f::text, $scale, '0')"; + final increment = _increment( + nonzero: 'r <> 0', + above: 'r > b - r', + tie: 'r = b - r', + odd: 'mod(${scale == 0 ? 'q' : 'f'}, 2) <> 0', + ); + // Usually r*factor fits NUMERIC. For a huge remainder, split b=d*factor+e + // instead. Then c=div(r,d) overestimates div(r*factor,b) by at most one: + // the fallback implies d >= 10^(131072-2*scale) > factor^2. All products + // below stay within the original denominator's magnitude or factor^2. + return '''($prefix, +orm_decimal_scaled AS MATERIALIZED ( + SELECT *, length(trunc(r)::text) + $scale <= ${Decimal.maxIntegerDigits} AS simple + FROM orm_decimal_remainder +), orm_decimal_split AS MATERIALIZED ( + SELECT *, CASE WHEN simple THEN r * $factor ELSE r END AS n, + CASE WHEN simple THEN b ELSE div(b, $factor) END AS d, + CASE WHEN simple THEN 0 ELSE mod(b, $factor) END AS e + FROM orm_decimal_scaled +), orm_decimal_estimate AS MATERIALIZED ( + SELECT *, div(n, NULLIF(d, 0)) AS c FROM orm_decimal_split +), orm_decimal_correction AS MATERIALIZED ( + SELECT *, CASE WHEN simple THEN n - c * d + ELSE (n - c * d) * $factor - c * e END AS tail + FROM orm_decimal_estimate +), orm_decimal_final AS ( + SELECT q, b, sgn, nil, + CASE WHEN tail < 0 THEN c - 1 ELSE c END AS f, + CASE WHEN tail < 0 THEN tail + b ELSE tail END AS r + FROM orm_decimal_correction +) +SELECT CASE WHEN nil THEN NULL WHEN b = 0 THEN 1 / b + ${_inexact('r <> 0')} + ELSE sgn * (CAST($text AS NUMERIC) + CASE WHEN $increment THEN $unit ELSE 0 END) +END FROM orm_decimal_final)'''; + } + + String _negative(String prefix) { + final places = -scale, half = "NUMERIC '5e${-scale - 1}'"; + final increment = _increment( + nonzero: 'lost <> 0 OR r <> 0', + above: 'lost > $half OR (lost = $half AND r <> 0)', + tie: 'lost = $half AND r = 0', + odd: 'mod(kept, 2) <> 0', + ); + // Construct integer digits as text so even the outermost supported scale + // (-131072) can return zero without first constructing an overflowing unit. + return '''($prefix, +orm_decimal_integer AS MATERIALIZED ( + SELECT *, CAST(COALESCE(NULLIF(left(q::text, greatest(length(q::text) - $places, 0)), ''), '0') AS NUMERIC) AS kept + FROM orm_decimal_remainder +), orm_decimal_truncated AS MATERIALIZED ( + SELECT *, CAST(kept::text || repeat('0', $places) AS NUMERIC) AS truncated + FROM orm_decimal_integer +), orm_decimal_final AS ( + SELECT *, q - truncated AS lost FROM orm_decimal_truncated +) +SELECT CASE WHEN nil THEN NULL WHEN b = 0 THEN 1 / b + ${_inexact('lost <> 0 OR r <> 0')} + ELSE sgn * CAST((kept + CASE WHEN $increment THEN 1 ELSE 0 END)::text || repeat('0', $places) AS NUMERIC) +END FROM orm_decimal_final)'''; + } + + String _inexact(String nonzero) => rounding == DecimalRounding.exact + ? "WHEN $nonzero THEN CAST(q::text || '_decimal_rounding_required' AS NUMERIC)" + : ''; + + String _increment({ + required String nonzero, + required String above, + required String tie, + required String odd, + }) => switch (rounding) { + DecimalRounding.exact || DecimalRounding.towardZero => 'FALSE', + DecimalRounding.floor => 'sgn < 0 AND ($nonzero)', + DecimalRounding.ceiling => 'sgn > 0 AND ($nonzero)', + DecimalRounding.halfAwayFromZero => '($above) OR ($tie)', + DecimalRounding.halfEven => '($above) OR (($tie) AND ($odd))', + }; +} + +/// @nodoc +@internal +final class DecimalAverage( + final SqlNode child, + final int scale, + final DecimalRounding rounding, +) extends SqlNode { + @override + String writeSql(SqlWriter w) => writeAverage(w); + + String writeAverage(SqlWriter w, {WindowNode? window}) { + if (w.mysql) { + throw const OrmException( + 'CAPABILITY.DECIMAL_ROUNDING', + 'Exact decimal averages are not supported on MySQL/MariaDB.', + ); + } + if (w.averageInputs == null) { + throw const OrmException( + 'QUERY.AGGREGATE', + 'An average requires a SELECT query; use a scalar subquery in assignments.', + ); + } + if (w.dialect == SqlDialect.sqlite) { + final function = FunctionNode('orm_decimal_avg_v1', [ + child, + ParameterNode(scale), + ParameterNode(rounding.index), + ]); + return (window == null + ? function + : WindowNode( + function, + window.partition, + window.order, + window.frame, + )) + .writeSql(w); + } + final input = AverageInput(child); + final partition = [ + for (final e in window?.partition ?? >[]) + Expr.internal(AverageInput(e.expressionNode), e.codec), + ]; + final order = [ + for (final o in window?.order ?? []) + OrderTerm.internal( + Expr.internal( + AverageInput(o.expression.expressionNode), + o.expression.codec, + ), + o.descending, + o.nulls, + ), + ]; + String aggregate(String name, SqlNode argument) { + final function = FunctionNode(name, [argument]); + return (window == null + ? function + : WindowNode(function, partition, order, window.frame)) + .write(w); + } + + // B exceeds every possible COUNT (signed int64). Each SUM stays finite: + // high has 20 fewer integer digits; low is bounded by COUNT*B. Native SUM + // preserves its exact accumulator without holding an array of input rows. + const base = "NUMERIC '1e20'"; + final high = aggregate('SUM', RawNode(['div(', ', $base)'], [input])); + final low = aggregate('SUM', RawNode(['mod(', ', $base)'], [input])); + final count = aggregate('COUNT', input); + final prefix = + ''' +WITH orm_mean_args(h, l, n) AS MATERIALIZED ( + VALUES ($high, $low, CAST($count AS NUMERIC)) +), orm_mean_high AS MATERIALIZED ( + SELECT *, div(h, NULLIF(n, 0)) AS qh, mod(h, NULLIF(n, 0)) AS rh + FROM orm_mean_args +), orm_mean_tail AS MATERIALIZED ( + SELECT *, rh * $base + l AS t FROM orm_mean_high +), orm_mean_parts AS MATERIALIZED ( + SELECT n, qh * $base + div(t, NULLIF(n, 0)) AS q, + mod(t, NULLIF(n, 0)) AS r FROM orm_mean_tail +), orm_mean_normal AS MATERIALIZED ( + SELECT n, + CASE WHEN q > 0 AND r < 0 THEN q - 1 WHEN q < 0 AND r > 0 THEN q + 1 ELSE q END AS q, + CASE WHEN q > 0 AND r < 0 THEN r + n WHEN q < 0 AND r > 0 THEN r - n ELSE r END AS r + FROM orm_mean_parts +), orm_decimal_remainder AS MATERIALIZED ( + SELECT abs(q) AS q, abs(r) AS r, n AS b, n = 0 AS nil, + CASE WHEN q = 0 THEN sign(r) ELSE sign(q) END AS sgn + FROM orm_mean_normal +)'''; + return _DecimalRoundSql(scale, rounding).write(prefix); + } +} + +// Shared by the high/low/count aggregates (and by a window's frame expressions). +// Projection staging handles windows; ordinary aggregation uses a row lateral. +/// @nodoc +@internal +final class AverageInput(final SqlNode child) extends SqlNode { + @override + String writeSql(SqlWriter w) => w.averageInputs!.capture(this); +} + +/// @nodoc +@internal +final class AverageInputs(final SqlWriter writer, final String anchor) { + final List joins = []; + final Map _references = {}; + String capture(AverageInput input) => _references.putIfAbsent(input, () { + final name = writer.quote( + '_orm_avg_${writer.aliases.length}_${joins.length}', + ); + final saved = writer.project; + writer.project = null; + late String value; + try { + value = input.child.write(writer); + } finally { + writer.project = saved; + } + // The zero term keeps even a volatile, otherwise uncorrelated input tied + // to each source row. OFFSET prevents flattening/repeated evaluation. + joins.add( + ' CROSS JOIN LATERAL (SELECT ($value) + CAST($anchor IS NULL AS INT) * NUMERIC \'0\' AS v OFFSET 0) AS $name', + ); + return '$name.v'; + }); +} diff --git a/lib/src/plan.dart b/lib/src/query/plan.dart similarity index 64% rename from lib/src/plan.dart rename to lib/src/query/plan.dart index c4318378..8a36f35b 100644 --- a/lib/src/plan.dart +++ b/lib/src/query/plan.dart @@ -1,4 +1,13 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import 'context.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'reads.dart'; +import 'relation.dart'; +import 'selection.dart'; +import 'table.dart'; /// A physical SQL output slot, including association keys and presence markers. /// Expressions without a direct column source have null table/column names. @@ -9,27 +18,47 @@ typedef PlannedColumn = ({ String? column, bool presence, }); + +/// A joined table name, join kind and whether a relation introduced it. typedef PlannedJoin = ({String table, bool left, bool relation}); /// A non-executing description of one SQL statement and its dependent batches. /// Bound values are omitted; literal SQL text is retained. A batch SQL template /// contains one parent key tuple. final class QueryPlan { + /// The dialect used to compile this statement. final SqlDialect dialect; + + /// Compiled SQL text without bound parameter values. final String sql; + + /// The number of separately bound values. final int parameterCount; + + /// Physical output slots, including internal association and presence keys. final List columns; + + /// Explicit and relation-generated joins in the root statement. final List joins; + + /// Conditional relationship loads that may execute extra statements. final List loads; + + /// Sorted physical table names read by this statement. final List reads; + + /// Whether raw SQL may read tables that cannot be inferred. final bool opaqueReads; - QueryPlan._( + + /// @nodoc + @internal + QueryPlan.internal( this.dialect, SqlCommand command, List columns, List joins, List loads, - _ReadTables reads, + ReadTables reads, ) : sql = command.sql, parameterCount = command.parameters.length, columns = List.unmodifiable(columns), @@ -48,6 +77,7 @@ final class QueryPlan { (count, load) => count + (load.query?.sqlTemplateCount ?? 0), ); + /// Returns an inspection report without parameter values. Map toJson() => { 'dialect': dialect.name, 'sql': sql, @@ -80,17 +110,33 @@ final class QueryPlan { /// One batch per chunk of distinct non-null parent keys, conditional on data. /// For nested loads, this rule applies separately to each returned parent batch. final class RelationLoadPlan { + /// Root output positions containing the relationship's parent key. final List parentColumns; + + /// Child output positions containing the matching foreign key. final List childColumns; + + /// The number of columns in each relationship key. final int keyWidth; + + /// An optional row limit applied separately to each parent. final int? limitPerParent; + + /// An optional row offset applied separately to each parent. final int offsetPerParent; + + /// Bound parameters needed before adding parent key tuples. final int? fixedParameters; + + /// Maximum parent keys that fit within the engine's parameter limit. final int? maxKeysPerBatch; /// Null when take(0) skips this relationship entirely. final QueryPlan? query; - RelationLoadPlan._( + + /// @nodoc + @internal + RelationLoadPlan.internal( List parentColumns, List childColumns, this.keyWidth, @@ -101,7 +147,11 @@ final class RelationLoadPlan { this.query, ) : parentColumns = List.unmodifiable(parentColumns), childColumns = List.unmodifiable(childColumns); + + /// Whether this load is known to produce no rows without executing SQL. bool get skipped => query == null; + + /// Returns a serializable description of the conditional load. Map toJson() => { 'strategy': 'batch', 'parentColumns': parentColumns, @@ -116,26 +166,37 @@ final class RelationLoadPlan { }; } -QueryPlan _inspectQuery(Query query) { - final plan = query._plan().$1, reads = _ReadTables(includeRaw: true); - final command = query._compile(plan, reads: reads); - return _describeStatement(query.database, query._state, plan, command, reads); +/// @nodoc +@internal +QueryPlan inspectQuery(Query query) { + final plan = query.planQuery().$1, reads = ReadTables(includeRaw: true); + final command = query.compileQuery(plan, reads: reads); + return _describeStatement( + query.database, + query.queryState, + plan, + command, + reads, + ); } QueryPlan _describeStatement( QueryContext db, - _QueryState state, - _SelectionPlan plan, + QueryState state, + SelectionPlan plan, SqlCommand command, - _ReadTables reads, + ReadTables reads, ) { final loads = [for (final load in plan.relations) load.inspect(db)]; final columns = []; for (final (i, expression) in plan.columns.indexed) { - final node = _unwrapStorage(expression._node); + final node = unwrapStorage(expression.expressionNode); final (table, column) = switch (node) { - _ColumnNode() => (node.table.schema.name, node.name), - _Presence() => (node.alias.fields.table.schema.name, node.alias._marker), + ColumnNode() => (node.table.schema.name, node.name), + PresenceNode() => ( + node.alias.fields.table.schema.name, + node.alias.presenceMarker, + ), _ => (null, null), }; columns.add(( @@ -143,10 +204,10 @@ QueryPlan _describeStatement( codecType: expression.codec.sqlType, table: table, column: column, - presence: node is _Presence, + presence: node is PresenceNode, )); } - return QueryPlan._( + return QueryPlan.internal( db.dialect, command, columns, @@ -169,14 +230,16 @@ QueryPlan _describeStatement( ); } -RelationLoadPlan _inspectRelation( +/// @nodoc +@internal +RelationLoadPlan inspectRelation( QueryContext db, - _TypedRelationBinding binding, + TypedRelationBinding binding, ) { - final relation = binding.relation, width = relation._child.length; - final state = relation._state; + final relation = binding.relation, width = relation.childFields.length; + final state = relation.queryState; if (state.limit == 0) { - return RelationLoadPlan._( + return RelationLoadPlan.internal( binding.parentIndices, const [], width, @@ -187,15 +250,17 @@ RelationLoadPlan _inspectRelation( null, ); } - final plan = _SelectionPlan(); + final plan = SelectionPlan(); // Bind descriptions, never decode a fake row. - relation._selection._bind(plan); - final childIndices = [for (final key in relation._child) plan.column(key)]; - final reads = _ReadTables(includeRaw: true); + relation.querySelection.bindSelection(plan); + final childIndices = [ + for (final key in relation.childFields) plan.column(key), + ]; + final reads = ReadTables(includeRaw: true); // Null placeholders require no codec or fabricated domain object. The command // is a template only and is never sent to a driver. - final command = binding._compile(db, plan, [ - _RelationKey(List.filled(width, null)), + final command = binding.compileQuery(db, plan, [ + RelationKey(List.filled(width, null)), ], reads: reads); final fixed = command.parameters.length - width; final capacity = (db.capabilities.maxParameters - fixed) ~/ width; @@ -205,7 +270,7 @@ RelationLoadPlan _inspectRelation( 'No parameter capacity remains for relation keys.', ); } - return RelationLoadPlan._( + return RelationLoadPlan.internal( binding.parentIndices, childIndices, width, diff --git a/lib/src/query.dart b/lib/src/query/query.dart similarity index 51% rename from lib/src/query.dart rename to lib/src/query/query.dart index 5fc9b30d..5a4183db 100644 --- a/lib/src/query.dart +++ b/lib/src/query/query.dart @@ -1,6 +1,24 @@ -part of '../sql.dart'; - -final class _QueryState { +import 'package:meta/meta.dart'; + +import '../../driver.dart'; +import '../../schema_model.dart'; +import 'batch.dart'; +import 'context.dart'; +import 'cte.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'mutation.dart'; +import 'nodes.dart'; +import 'plan.dart'; +import 'reads.dart'; +import 'relation.dart'; +import 'selection.dart'; +import 'table.dart'; +import 'union.dart'; + +/// @nodoc +@internal +final class QueryState { final TableRef source; final Expr? predicate; final List order; @@ -9,10 +27,10 @@ final class _QueryState { final int? limit; final int? offset; final bool distinct; - final List<_Join> joins; - final List<_CteDefinition> ctes; - final _UnionSource? union; - const _QueryState( + final List joins; + final List ctes; + final UnionSource? union; + const QueryState( this.source, { this.predicate, this.order = const [], @@ -25,7 +43,7 @@ final class _QueryState { this.ctes = const [], this.union, }); - _QueryState copy({ + QueryState copy({ Expr? predicate, List? order, List>? group, @@ -33,9 +51,9 @@ final class _QueryState { int? limit, int? offset, bool? distinct, - List<_Join>? joins, - List<_CteDefinition>? ctes, - }) => _QueryState( + List? joins, + List? ctes, + }) => QueryState( source, predicate: predicate ?? this.predicate, order: order ?? this.order, @@ -50,97 +68,155 @@ final class _QueryState { ); } +/// An immutable typed query bound to a SQL context. +/// +/// Composition methods return new descriptions. [compile] and [inspect] perform +/// no I/O; reads execute only with a bound database or session. [select] changes +/// the result type without changing the typed source fields. class Query { + /// The compilation or execution context that owns this query. final QueryContext database; - final F _fields; - final _QueryState _state; - final Selection _selection; - Query._(this.database, this._fields, this._state, this._selection); - Query _copy(_QueryState state) => - Query._(database, _fields, state, _selection); + /// @nodoc + @internal + final F queryFields; + + /// @nodoc + @internal + final QueryState queryState; + + /// @nodoc + @internal + final Selection querySelection; + + /// @nodoc + @internal + Query.internal( + this.database, + this.queryFields, + this.queryState, + this.querySelection, + ); + + /// @nodoc + @internal + Query copyQuery(QueryState state) => + Query.internal(database, queryFields, state, querySelection); + + /// Adds a predicate with AND, preserving any existing filter. Query where(Expr Function(F) condition) { - final next = condition(_fields); - return _copy(_state.copy(predicate: _state.predicate?.and(next) ?? next)); + final next = condition(queryFields); + return copyQuery( + queryState.copy(predicate: queryState.predicate?.and(next) ?? next), + ); } + /// Replaces the query's ordering with the supplied expressions. Query orderBy(List Function(F) order) => - _copy(_state.copy(order: List.unmodifiable(order(_fields)))); + copyQuery(queryState.copy(order: List.unmodifiable(order(queryFields)))); + + /// Replaces SQL grouping expressions; selected non-aggregates must be grouped. Query groupBy(List> Function(F) group) => - _copy(_state.copy(group: List.unmodifiable(group(_fields)))); + copyQuery(queryState.copy(group: List.unmodifiable(group(queryFields)))); + + /// Adds an aggregate filter with AND to the existing HAVING clause. Query having(Expr Function(F) condition) => - _copy(_state.copy(having: condition(_fields))); + copyQuery(queryState.copy(having: condition(queryFields))); + + /// Replaces the row limit; count must be nonnegative. Query take(int count) { if (count < 0) throw ArgumentError.value(count, 'count'); - return _copy(_state.copy(limit: count)); + return copyQuery(queryState.copy(limit: count)); } + /// Replaces the row offset; count must be nonnegative. Query skip(int count) { if (count < 0) throw ArgumentError.value(count, 'count'); - return _copy(_state.copy(offset: count)); + return copyQuery(queryState.copy(offset: count)); } - Query distinct() => _copy(_state.copy(distinct: true)); + /// Removes duplicate SQL projections before any Dart mapping. + Query distinct() => copyQuery(queryState.copy(distinct: true)); + + /// Chooses the SQL columns and result type to decode. Query select(Selection Function(F) selection) => - Query._(database, _fields, _state, selection(_fields)); + Query.internal(database, queryFields, queryState, selection(queryFields)); /// Maps decoded rows in Dart. This does not create SQL columns or change /// database DISTINCT/UNION semantics; compose SQL set operations first. - Query map(S Function(R) mapper) => - Query._(database, _fields, _state, _selection.map(mapper)); + Query map(S Function(R) mapper) => Query.internal( + database, + queryFields, + queryState, + querySelection.map(mapper), + ); + /// Adds an inner join using an independently created table alias. Query join( TableAlias alias, { required Expr Function(F, G) on, - }) => _join(alias, on(_fields, alias.fields), false); + }) => _join(alias, on(queryFields, alias.fields), false); + + /// Adds a left join; use the alias's optional selection for absent rows. Query leftJoin( TableAlias alias, { required Expr Function(F, G) on, - }) => _join(alias, on(_fields, alias.fields), true); + }) => _join(alias, on(queryFields, alias.fields), true); Query _join( TableAlias alias, Expr on, bool left, ) { - if (alias.fields.table == _state.source || - _state.joins.any((j) => j.alias.fields.table == alias.fields.table)) { + if (alias.fields.table == queryState.source || + queryState.joins.any( + (j) => j.alias.fields.table == alias.fields.table, + )) { throw const OrmException( 'QUERY.ALIAS', 'Create a fresh alias for each table occurrence.', ); } - return _copy( - _state.copy( - joins: List.unmodifiable([..._state.joins, _Join(alias, on, left)]), + return copyQuery( + queryState.copy( + joins: List.unmodifiable([...queryState.joins, Join(alias, on, left)]), ), ); } + /// Converts a scalar selection into a nullable SQL subquery. + /// + /// Requires `take(1)` or an ungrouped aggregate. This builds an expression and + /// does not execute the subquery independently. Expr scalar() { - final selection = _selection; + final selection = querySelection; if (selection is! Expr) { throw const OrmException( 'QUERY.SCALAR', 'A scalar subquery selects one SQL expression.', ); } - if (_state.limit != 1 && - !(_state.group.isEmpty && _aggregate(selection._node))) { + if (queryState.limit != 1 && + !(queryState.group.isEmpty && aggregate(selection.expressionNode))) { throw const OrmException( 'QUERY.SCALAR', 'Use take(1) or an ungrouped aggregate for a scalar subquery.', ); } - return Expr._(_Subquery(this), selection.codec.nullable()); + return Expr.internal(SubqueryNode(this), selection.codec.nullable()); } + /// Builds a SQL EXISTS expression without executing a read. Expr existsExpression() => - Expr._(_Subquery(this, exists: true), Codecs.boolean); - Cte asCte(String name) => Cte._(this, name); + Expr.internal(SubqueryNode(this, exists: true), Codecs.boolean); + + /// Names this query as a CTE for further typed composition. + Cte asCte(String name) => Cte.internal(this, name); - (_SelectionPlan, _Decoder) _plan({bool deduplicate = true}) { - final plan = _SelectionPlan(deduplicate: deduplicate); - final decode = _selection._bind(plan); + /// @nodoc + @internal + (SelectionPlan, RowDecoder) planQuery({bool deduplicate = true}) { + final plan = SelectionPlan(deduplicate: deduplicate); + final decode = querySelection.bindSelection(plan); if (plan.columns.isEmpty) { throw const OrmException( 'QUERY.EMPTY_SELECTION', @@ -150,8 +226,10 @@ class Query { return (plan, decode); } - SqlCommand _compile(_SelectionPlan plan, {_ReadTables? reads}) { - final w = _Writer( + /// @nodoc + @internal + SqlCommand compileQuery(SelectionPlan plan, {ReadTables? reads}) { + final w = SqlWriter( database.dialect, {}, database: database, @@ -159,18 +237,20 @@ class Query { exactDecimal: database.capabilities.exactDecimal, temporal: database.capabilities.temporal, ); - return w.finish(_write(w, plan, decodeResult: true)); + return w.finish(writeQuery(w, plan, decodeResult: true)); } - String _write( - _Writer w, - _SelectionPlan plan, { + /// @nodoc + @internal + String writeQuery( + SqlWriter w, + SelectionPlan plan, { bool aliasColumns = false, bool decodeResult = false, }) { if (w.mysql && decodeResult && - _state.distinct && + queryState.distinct && plan.columns.any((e) => e.codec.sqlType == 'json')) { throw const OrmException( 'CAPABILITY.JSON_DISTINCT', @@ -183,13 +263,15 @@ class Query { 'Compose queries from the same database or transaction session.', ); } - final joins = [..._state.joins, ...plan.joins]; - if (_state.union == null && - !_state.ctes.any((cte) => cte.name == _state.source.schema.name)) { - w.reads?.tables.add(_state.source.schema); + final joins = [...queryState.joins, ...plan.joins]; + if (queryState.union == null && + !queryState.ctes.any( + (cte) => cte.name == queryState.source.schema.name, + )) { + w.reads?.tables.add(queryState.source.schema); } for (final join in joins) { - if (join.alias._cte == null) { + if (join.alias.cteDefinition == null) { w.reads?.tables.add(join.alias.fields.table.schema); } } @@ -201,25 +283,25 @@ class Query { } if (!database.capabilities.windowFunctions && [ - ...plan.columns.map((e) => e._node), - ..._state.order.map((o) => o.expression._node), - ].any(_window)) { + ...plan.columns.map((e) => e.expressionNode), + ...queryState.order.map((o) => o.expression.expressionNode), + ].any(window)) { throw const OrmException( 'CAPABILITY.WINDOW', 'This driver does not support window functions.', ); } - _validateGrouping(_state.copy(joins: joins), plan); + validateGrouping(queryState.copy(joins: joins), plan); final saved = Map.of(w.aliases); final markers = Set.of(w.leftJoins); - if (w.aliases.containsKey(_state.source)) { + if (w.aliases.containsKey(queryState.source)) { throw const OrmException( 'QUERY.ALIAS', 'A nested query needs a fresh source occurrence.', ); } - w.aliases[_state.source] = 't${w.aliases.length}'; - final rootAlias = w.aliases[_state.source]!; + w.aliases[queryState.source] = 't${w.aliases.length}'; + final rootAlias = w.aliases[queryState.source]!; for (final join in joins) { if (w.aliases.containsKey(join.alias.fields.table)) { throw const OrmException( @@ -231,18 +313,18 @@ class Query { if (join.left) w.leftJoins.add(join.alias.fields.table); } final savedAverageInputs = w.averageInputs; - w.averageInputs = _AverageInputs( + w.averageInputs = AverageInputs( w, - _state.source.schema.columns.isEmpty + queryState.source.schema.columns.isEmpty ? w.quote(rootAlias) - : '${w.quote(rootAlias)}.${w.quote(_state.source.schema.columns.first.name)}', + : '${w.quote(rootAlias)}.${w.quote(queryState.source.schema.columns.first.name)}', ); try { final buffer = StringBuffer(); - final ctes = {}; + final ctes = {}; for (final cte in [ - ..._state.ctes, - for (final join in joins) ?join.alias._cte, + ...queryState.ctes, + for (final join in joins) ?join.alias.cteDefinition, ]) { if (ctes.containsKey(cte.name) && !identical(ctes[cte.name], cte)) { throw const OrmException( @@ -254,20 +336,20 @@ class Query { } if (ctes.isNotEmpty) { buffer.write( - 'WITH ${ctes.values.map((c) => c._writeDefinition(w)).join(', ')} ', + 'WITH ${ctes.values.map((c) => c.writeDefinition(w)).join(', ')} ', ); } final stage = w.dialect == SqlDialect.postgres && [ - ...plan.columns.map((e) => e._node), - ..._state.order.map((o) => o.expression._node), + ...plan.columns.map((e) => e.expressionNode), + ...queryState.order.map((o) => o.expression.expressionNode), ].any(_needsWindowStage) ? _WindowStage( w, preWindow: [ - ...plan.columns.map((e) => e._node), - ..._state.order.map((o) => o.expression._node), + ...plan.columns.map((e) => e.expressionNode), + ...queryState.order.map((o) => o.expression.expressionNode), ].any(_averageWindow), ) : null; @@ -277,66 +359,69 @@ class Query { try { for (var i = 0; i < plan.columns.length; i++) { columns.add( - '${decodeResult ? _projection(plan.columns[i], w) : plan.columns[i]._node.write(w)}${aliasColumns ? ' AS ${w.quote('c$i')}' : ''}', + '${decodeResult ? projection(plan.columns[i], w) : plan.columns[i].expressionNode.write(w)}${aliasColumns ? ' AS ${w.quote('c$i')}' : ''}', ); } - for (final term in _state.order) { + for (final term in queryState.order) { if (stage != null) { // PostgreSQL DISTINCT requires ORDER BY to reuse selected expressions. final match = plan.columns.indexWhere( - (e) => _sameSqlNode(e._node, term.expression._node), + (e) => + sameSqlNode(e.expressionNode, term.expression.expressionNode), ); order.add( - match < 0 ? term._write(w) : '${match + 1}${term._suffix}', + match < 0 + ? term.writeQuery(w) + : '${match + 1}${term.orderSuffix}', ); } else { - order.add(term._write(w)); + order.add(term.writeQuery(w)); } } } finally { w.project = null; } buffer.write( - 'SELECT ${stage == null && _state.distinct ? 'DISTINCT ' : ''}', + 'SELECT ${stage == null && queryState.distinct ? 'DISTINCT ' : ''}', ); buffer.write( stage == null ? columns.join(', ') : stage.inputs.join(', '), ); buffer.write( - ' FROM ${_state.union == null ? w.quote(_state.source.schema.name) : '(${_state.union!.write(w)})'} AS ${w.quote(rootAlias)}', + ' FROM ${queryState.union == null ? w.quote(queryState.source.schema.name) : '(${queryState.union!.write(w)})'} AS ${w.quote(rootAlias)}', ); - final visible = {...saved, _state.source: rootAlias}; + final visible = {...saved, queryState.source: rootAlias}; for (final join in joins) { final ref = join.alias.fields.table; final alias = w.aliases[ref]!; visible[ref] = alias; - final check = _Writer( + final check = SqlWriter( w.dialect, visible, database: w.database, exactDecimal: w.exactDecimal, temporal: w.temporal, )..leftJoins.addAll(w.leftJoins.where(visible.containsKey)); - join.on._node.write(check); + join.on.expressionNode.write(check); final table = w.quote(ref.schema.name); final source = join.left - ? '(SELECT *, 1 AS ${w.quote(join.alias._marker)} FROM $table)' + ? '(SELECT *, 1 AS ${w.quote(join.alias.presenceMarker)} FROM $table)' : table; buffer.write( - ' ${join.left ? 'LEFT JOIN' : 'JOIN'} $source AS ${w.quote(alias)} ON ${join.on._node.write(w)}', + ' ${join.left ? 'LEFT JOIN' : 'JOIN'} $source AS ${w.quote(alias)} ON ${join.on.expressionNode.write(w)}', ); } final joinEnd = buffer.length; - if (_state.predicate case final predicate?) { - buffer.write(' WHERE ${predicate._node.write(w)}'); + if (queryState.predicate case final predicate?) { + buffer.write(' WHERE ${predicate.expressionNode.write(w)}'); } - if (_state.group.isNotEmpty) { + if (queryState.group.isNotEmpty) { buffer.write( - ' GROUP BY ${_state.group.map((e) => e._node.write(w)).join(', ')}', + ' GROUP BY ${queryState.group.map((e) => e.expressionNode.write(w)).join(', ')}', ); } - if (_state.having case final having?) { - buffer.write(' HAVING ${having._node.write(w)}'); + if (queryState.having case final having?) { + buffer.write(' HAVING ${having.expressionNode.write(w)}'); } if (w.averageInputs!.joins.isNotEmpty) { final text = buffer.toString(); @@ -349,17 +434,17 @@ class Query { final inner = stage.wrap(buffer.toString()); buffer.clear(); buffer.write( - 'SELECT ${_state.distinct ? 'DISTINCT ' : ''}${columns.join(', ')} FROM ($inner) AS ${w.quote(stage.alias)}', + 'SELECT ${queryState.distinct ? 'DISTINCT ' : ''}${columns.join(', ')} FROM ($inner) AS ${w.quote(stage.alias)}', ); } - if (_state.order.isNotEmpty) { + if (queryState.order.isNotEmpty) { buffer.write(' ORDER BY ${order.join(', ')}'); } - if (_state.limit case final limit?) { + if (queryState.limit case final limit?) { buffer.write(' LIMIT ${w.parameter(limit)}'); } - if (_state.offset case final offset?) { - if (_state.limit == null) { + if (queryState.offset case final offset?) { + if (queryState.limit == null) { if (database.dialect == SqlDialect.sqlite) buffer.write(' LIMIT -1'); if (w.mysql) buffer.write(' LIMIT 18446744073709551615'); } @@ -377,34 +462,39 @@ class Query { } } - SqlCommand compile() => _compile(_plan().$1); + /// Validates and compiles SQL with separately bound parameters, without I/O. + SqlCommand compile() => compileQuery(planQuery().$1); /// Inspects SQL and conditional relation batches without acquiring a connection. /// This does not ask the database optimizer for an execution plan. - QueryPlan inspect() => _inspectQuery(this); + QueryPlan inspect() => inspectQuery(this); /// Physical read dependencies used by subscriptions, including relation batches. ({Set tables, bool opaque}) get dependencies { - final reads = _ReadTables()..query(this); + final reads = ReadTables()..query(this); return (tables: Set.unmodifiable(reads.tables), opaque: reads.opaque); } /// Rebind the root description to an execution context. Nested queries retain /// their original context and are rejected when they do not match. Query bind(QueryContext context) => - Query._(context, _fields, _state, _selection); + Query.internal(context, queryFields, queryState, querySelection); + /// Executes the query and decodes all selected rows. + /// + /// Relation batches reuse the same connection. Use an explicit transaction when + /// a consistent snapshot across multiple statements is required. Future> get({ExecutionOptions options = const ExecutionOptions()}) { options.check(); return database.run((connection) async { - final (plan, decode) = _plan(); - final command = _compile(plan); + final (plan, decode) = planQuery(); + final command = compileQuery(plan); final result = await database.executeOn( connection, command, options: options, ); - final rows = await _expandRelations( + final rows = await expandRelations( database, connection, plan, @@ -419,18 +509,42 @@ class Query { }, acquire: options.acquisition); } - Future first({ + /// Returns the first selected row, or throws `QUERY.CARDINALITY` if empty. + /// + /// Fetches at most one root row. Use [orderBy] for a deterministic first row. + /// A selected SQL NULL is a row and is returned when `R` is nullable. + Future first({ExecutionOptions options = const ExecutionOptions()}) async { + final rows = await take(queryState.limit == 0 ? 0 : 1) + .get(options: options); + if (rows.isEmpty) { + throw const OrmException( + 'QUERY.CARDINALITY', + 'Expected a row, received none.', + ); + } + return rows.first; + } + + /// Returns the first selected row, or null when the query is empty. + /// + /// Fetches at most one root row. A nullable selection also returns null for a + /// row containing SQL NULL; select a Record to distinguish these cases. + Future firstOrNull({ ExecutionOptions options = const ExecutionOptions(), }) async { - final rows = await take(_state.limit == 0 ? 0 : 1).get(options: options); + final rows = await take(queryState.limit == 0 ? 0 : 1) + .get(options: options); return rows.isEmpty ? null : rows.first; } + /// Returns exactly one selected row or throws `QUERY.CARDINALITY`. + /// + /// Fetches at most two root rows to detect an ambiguous result. Future single({ ExecutionOptions options = const ExecutionOptions(), }) async { final rows = await take( - _state.limit == null || _state.limit! > 2 ? 2 : _state.limit!, + queryState.limit == null || queryState.limit! > 2 ? 2 : queryState.limit!, ).get(options: options); if (rows.length != 1) { throw OrmException( @@ -441,13 +555,32 @@ class Query { return rows.single; } + /// Returns the only selected row, or null when empty. + /// + /// Throws `QUERY.CARDINALITY` for more than one row, fetching at most two. + Future singleOrNull({ + ExecutionOptions options = const ExecutionOptions(), + }) async { + final rows = await take( + queryState.limit == null || queryState.limit! > 2 ? 2 : queryState.limit!, + ).get(options: options); + if (rows.length > 1) { + throw OrmException( + 'QUERY.CARDINALITY', + 'Expected at most one row, received ${rows.length}.', + ); + } + return rows.firstOrNull; + } + + /// Counts the query's result rows, including its limit, offset and grouping. Future count({ ExecutionOptions options = const ExecutionOptions(), }) async { final inner = compile(); final result = await database.execute( SqlCommand( - 'SELECT COUNT(*) FROM (${inner.sql}) AS ${_Writer(database.dialect, {}).quote('orm_count')}', + 'SELECT COUNT(*) FROM (${inner.sql}) AS ${SqlWriter(database.dialect, {}).quote('orm_count')}', inner.parameters, ), options: options, @@ -455,10 +588,11 @@ class Query { return Codecs.integer.decode(result.rows.single.single); } + /// Checks whether the current query yields at least one row. Future exists({ ExecutionOptions options = const ExecutionOptions(), }) async { - final inner = take(_state.limit == 0 ? 0 : 1).compile(); + final inner = take(queryState.limit == 0 ? 0 : 1).compile(); final result = await database.execute( SqlCommand('SELECT EXISTS (${inner.sql})', inner.parameters), options: options, @@ -466,15 +600,24 @@ class Query { return Codecs.boolean.decode(result.rows.single.single); } - Mutation update(List Function(F) assignments) => Mutation._( + /// Prepares an update; call the mutation's execute or returning method. + Mutation update(List Function(F) assignments) => + Mutation.internal( + database, + queryFields, + queryState, + MutationKind.update, + assignments(queryFields), + ); + + /// Prepares a delete; no SQL runs until the mutation is executed. + Mutation delete() => Mutation.internal( database, - _fields, - _state, - _MutationKind.update, - assignments(_fields), + queryFields, + queryState, + MutationKind.delete, + const [], ); - Mutation delete() => - Mutation._(database, _fields, _state, _MutationKind.delete, const []); } // A scalar SQL subquery cannot contain an enclosing query's window function. @@ -483,29 +626,33 @@ class Query { // ordering and pagination apply to the final values. This remains one statement. // Window averages additionally share their input before the native component // windows run, including when the input is itself a grouped aggregate. -bool _needsWindowStage(_Node node) => - node is _WindowNode && node.function is _DecimalAverage || - node is _DecimalRatio && _children(node).any(_window) || - _children(node).any(_needsWindowStage); - -bool _averageWindow(_Node node) => - node is _WindowNode && node.function is _DecimalAverage || - _children(node).any(_averageWindow); -bool _averageInput(_Node node) => - node is _AverageInput || _children(node).any(_averageInput); - -final class _WindowStage(final _Writer writer, {final bool preWindow = false}) { +bool _needsWindowStage(SqlNode node) => + node is WindowNode && node.function is DecimalAverage || + node is DecimalRatio && children(node).any(window) || + children(node).any(_needsWindowStage); + +bool _averageWindow(SqlNode node) => + node is WindowNode && node.function is DecimalAverage || + children(node).any(_averageWindow); + +bool _averageInput(SqlNode node) => + node is AverageInput || children(node).any(_averageInput); + +final class _WindowStage( + final SqlWriter writer, { + final bool preWindow = false, +}) { late final String alias = '_orm_window_${writer.aliases.length}'; late final String inputAlias = '_orm_inputs_${writer.aliases.length}'; final List inputs = []; final List windows = []; - final Map<_AverageInput, String> _shared = {}; + final Map _shared = {}; String wrap(String sql) => preWindow ? 'SELECT ${windows.join(', ')} FROM ($sql) AS ${writer.quote(inputAlias)}' : sql; - String? capture(_Node node) { + String? capture(SqlNode node) { if (_needsWindowStage(node)) return null; final output = preWindow ? windows : inputs; final index = output.length; @@ -520,15 +667,15 @@ final class _WindowStage(final _Writer writer, {final bool preWindow = false}) { return '${writer.quote(alias)}.${writer.quote('w$index')}'; } - String? _capturePre(_Node node) { - if (node is _AverageInput) { + String? _capturePre(SqlNode node) { + if (node is AverageInput) { return _shared.putIfAbsent(node, () => _input(node.child)); } - if (node is _WindowNode || _averageInput(node)) return null; + if (node is WindowNode || _averageInput(node)) return null; return _input(node); } - String _input(_Node node) { + String _input(SqlNode node) { final index = inputs.length; final saved = writer.project; writer.project = null; @@ -541,36 +688,50 @@ final class _WindowStage(final _Writer writer, {final bool preWindow = false}) { } } +/// Typed access to a table, including full-row reads and prepared inserts. class TableSet extends Query { + /// The table definition used to create fields and decode full rows. final Table definition; + + /// Binds a table definition to a compilation or execution context. TableSet(QueryContext database, Table definition) - : this._( + : this.internal( database, definition, definition.createFields(TableRef(definition.schema)), ); - TableSet._(QueryContext database, this.definition, F fields) - : super._( + + /// @nodoc + @internal + TableSet.internal(QueryContext database, this.definition, F fields) + : super.internal( database, fields, - _QueryState(fields.table), + QueryState(fields.table), definition.selectRow(fields), ); - Mutation insert(List Function(F) assignments) => Mutation._( - database, - _fields, - _state, - _MutationKind.insert, - _insertDefaults(assignments(_fields)), - createFields: definition.createFields, - ); + /// Prepares one insert, evaluating omitted client defaults once now. + Mutation insert(List Function(F) assignments) => + Mutation.internal( + database, + queryFields, + queryState, + MutationKind.insert, + _insertDefaults(assignments(queryFields)), + fieldsFactory: definition.createFields, + ); + + /// Prepares rows for a parameter-aware batch insert. + /// + /// Client defaults run once per omitted field during preparation. Execution uses + /// one transaction across all generated statement chunks. BatchInsert insertMany( Iterable rows, List Function(F, T) values, - ) => BatchInsert._(database, _fields, _state, [ + ) => BatchInsert.internal(database, queryFields, queryState, [ for (final row in rows) - List.unmodifiable(_insertDefaults(values(_fields, row))), + List.unmodifiable(_insertDefaults(values(queryFields, row))), ]); List _insertDefaults(List assignments) { @@ -581,10 +742,14 @@ class TableSet extends Query { ...assignments, for (final column in defaults) if (!assigned.contains(column.name)) - _fields.column(column).set(column.clientDefault!()), + queryFields.column(column).set(column.clientDefault!()), ]; } + /// Inserts and returns a complete row. + /// + /// Uses RETURNING when supported. Otherwise inserts and reads the primary key on + /// one transaction; keys must be literal values or one generated identity. Future createRow( List Function(F) assignments, { ExecutionOptions options = const ExecutionOptions(), @@ -594,16 +759,16 @@ class TableSet extends Query { return mutation.returning(definition.selectRow).single(options: options); } final keys = definition.schema.primaryKey; - final values = {}; + final values = {}; final generated = {}; for (final key in keys) { final column = definition.schema.columns.firstWhere((c) => c.name == key); - final assignment = mutation._assignments + final assignment = mutation.writeAssignments .where((a) => a.field.definition.name == key) .firstOrNull; - if (assignment?._value case final _Parameter parameter) { + if (assignment?.assignedValue case final ParameterNode parameter) { values[key] = parameter; - } else if (column.generated && assignment?._value == null) { + } else if (column.generated && assignment?.assignedValue == null) { generated.add(key); } else { throw const OrmException( @@ -620,13 +785,13 @@ class TableSet extends Query { } Future create(QueryContext tx) async { try { - final bound = Mutation._( + final bound = Mutation.internal( tx, - mutation._fields, - mutation._state, - mutation._kind, - mutation._assignments, - createFields: mutation._createFields, + mutation.queryFields, + mutation.queryState, + mutation.mutationKind, + mutation.writeAssignments, + fieldsFactory: mutation.fieldsFactory, ); final result = await tx.executeCommand( bound.compile(), @@ -642,26 +807,29 @@ class TableSet extends Query { 'The driver did not return the generated identity.', ); } - values[generated.single] = _Parameter(result.lastInsertId); + values[generated.single] = ParameterNode(result.lastInsertId); } - _Node? predicate; + SqlNode? predicate; for (final key in keys) { - final equal = _Binary( - _ColumnNode(_state.source, key), + final equal = BinaryNode( + ColumnNode(queryState.source, key), '=', values[key]!, ); predicate = predicate == null ? equal - : _Binary(predicate, 'AND', equal); + : BinaryNode(predicate, 'AND', equal); } - return await Query._( + return await Query.internal( tx, - _fields, - _state.copy( - predicate: Expr._(predicate!, Codecs.boolean.nullable()), + queryFields, + queryState.copy( + predicate: Expr.internal( + predicate!, + Codecs.boolean.nullable(), + ), ), - definition.selectRow(_fields), + definition.selectRow(queryFields), ).single(options: options); } catch (_) { tx.markFailed(); diff --git a/lib/src/query_reads.dart b/lib/src/query/reads.dart similarity index 68% rename from lib/src/query_reads.dart rename to lib/src/query/reads.dart index 96d30bc0..10981d6a 100644 --- a/lib/src/query_reads.dart +++ b/lib/src/query/reads.dart @@ -1,16 +1,23 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; +import '../../schema_model.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'table.dart'; + +/// @nodoc +@internal /// SQL compilation visits subqueries and CTE definitions with the same writer. /// Batch relationships are additional queries and must be visited separately. -final class _ReadTables { +final class ReadTables { final bool includeRaw; - _ReadTables({this.includeRaw = false}); + ReadTables({this.includeRaw = false}); bool opaque = false; final Set tables = Set.identity(); void query(Query query) { - final (plan, _) = query._plan(); - query._write( - _Writer( + final (plan, _) = query.planQuery(); + query.writeQuery( + SqlWriter( query.database.dialect, {}, database: query.database, diff --git a/lib/src/relation.dart b/lib/src/query/relation.dart similarity index 53% rename from lib/src/relation.dart rename to lib/src/query/relation.dart index ece991ba..3d1b941a 100644 --- a/lib/src/relation.dart +++ b/lib/src/query/relation.dart @@ -1,17 +1,60 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; +import 'dart:typed_data'; + +import '../../driver.dart'; +import 'context.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'plan.dart'; +import 'query.dart'; +import 'reads.dart'; +import 'selection.dart'; +import 'table.dart'; + +/// How a to-one selection is loaded when its parent query executes. +/// +/// [automatic] joins targets covered by a declared primary or unique key and +/// batches other targets. [join] requires such a key; [batch] always uses +/// additional queries limited by the driver's available parameter capacity. enum ToOneStrategy { automatic, join, batch } /// A relationship is a query description. Constructing or selecting it performs /// no I/O. List results are loaded in batches on the root query's connection. +/// +/// Filters, ordering, pagination, and selections return new descriptions. Each +/// selected relationship can add queries; batching groups distinct parent keys +/// instead of issuing one query per parent. Use a transaction when the root and +/// its batches must share the database's configured snapshot isolation. final class Relation { final TableAlias _alias; - final List> _parent; - final List> _child; - final F _fields; - final _QueryState _state; - final Selection _selection; + /// @nodoc + @internal + final List> parentFields; + + /// @nodoc + @internal + final List> childFields; + + /// @nodoc + @internal + final F queryFields; + + /// @nodoc + @internal + final QueryState queryState; + + /// @nodoc + @internal + final Selection querySelection; + + /// Describes an equality relationship between parent and target key columns. + /// + /// [parent] and [child] must have the same nonzero arity. Child fields must + /// belong to the supplied [target] occurrence. A null parent key has no match. + /// Generated relationship getters normally construct this value for you. factory Relation( Table target, { required List> parent, @@ -27,48 +70,83 @@ final class Relation { 'Relationship keys need equal non-zero arity and fields from the target occurrence.', ); } - return Relation._( + return Relation.internal( alias, List.unmodifiable(parent), List.unmodifiable(keys), fields, - _QueryState(fields.table), + QueryState(fields.table), target.selectRow(fields), ); } - Relation._( + + /// @nodoc + @internal + Relation.internal( this._alias, - this._parent, - this._child, - this._fields, - this._state, - this._selection, + this.parentFields, + this.childFields, + this.queryFields, + this.queryState, + this.querySelection, + ); + + /// @nodoc + @internal + Relation copyQuery(QueryState state) => Relation.internal( + _alias, + parentFields, + childFields, + queryFields, + state, + querySelection, ); - Relation _copy(_QueryState state) => - Relation._(_alias, _parent, _child, _fields, state, _selection); + + /// Adds a SQL predicate to the related rows, combined with earlier filters. Relation where(Expr Function(F) predicate) { - final next = predicate(_fields); - return _copy(_state.copy(predicate: _state.predicate?.and(next) ?? next)); + final next = predicate(queryFields); + return copyQuery( + queryState.copy(predicate: queryState.predicate?.and(next) ?? next), + ); } + /// Replaces related-row ordering; include a stable tie breaker for pagination. Relation orderBy(List Function(F) order) => - _copy(_state.copy(order: List.unmodifiable(order(_fields)))); + copyQuery(queryState.copy(order: List.unmodifiable(order(queryFields)))); + + /// Limits each parent's result to [count] rows; negative counts are rejected. + /// + /// Batched per-parent pagination requires explicit [orderBy]. Relation take(int count) { if (count < 0) throw ArgumentError.value(count, 'count'); - return _copy(_state.copy(limit: count)); + return copyQuery(queryState.copy(limit: count)); } + /// Skips [count] rows per parent; batched pagination requires [orderBy]. Relation skip(int count) { if (count < 0) throw ArgumentError.value(count, 'count'); - return _copy(_state.copy(offset: count)); + return copyQuery(queryState.copy(offset: count)); } + /// Replaces each related row's result shape without loading it. Relation select(Selection Function(F) selection) => - Relation._(_alias, _parent, _child, _fields, _state, selection(_fields)); + Relation.internal( + _alias, + parentFields, + childFields, + queryFields, + queryState, + selection(queryFields), + ); + + /// Selects a list per parent, returning an empty list when no rows match. + /// + /// Execution batches distinct non-null parent keys according to the driver's + /// parameter limit. Nested collections can require further batches. Selection> many() => _RelationSelection(this); bool get _uniqueTarget { - final names = _child.map((key) => key.definition.name).toSet(); - final schema = _fields.table.schema; + final names = childFields.map((key) => key.definition.name).toSet(); + final schema = queryFields.table.schema; return [ schema.primaryKey, ...schema.uniqueKeys, @@ -87,18 +165,25 @@ final class Relation { return strategy != ToOneStrategy.batch && _uniqueTarget; } + /// Selects at most one visible row, or null when no row matches. + /// + /// A batch with multiple selected rows throws `RELATION.CARDINALITY`. + /// When `R` is nullable, a present row containing SQL NULL also returns null; + /// use [required] to require row presence independently of the selected value. Selection one({ToOneStrategy strategy = ToOneStrategy.automatic}) => _useJoin(strategy) ? _JoinedRelationSelection(this) : _oneBatch().map((rows) => rows.firstOrNull); Selection> _oneBatch() => - _copy( - _state.copy( - order: _state.order.isEmpty - ? [for (final key in _child) key.asc()] - : _state.order, - limit: _state.limit != null && _state.limit! < 2 ? _state.limit : 2, + copyQuery( + queryState.copy( + order: queryState.order.isEmpty + ? [for (final key in childFields) key.asc()] + : queryState.order, + limit: queryState.limit != null && queryState.limit! < 2 + ? queryState.limit + : 2, ), ).many().map((rows) { if (rows.length > 1) { @@ -110,6 +195,11 @@ final class Relation { return rows; }); + /// Requires one visible row while preserving the selected value's nullability. + /// + /// Throws `RELATION.MISSING` for an absent row and `RELATION.CARDINALITY` for + /// multiple selected rows. This validates row presence during decoding; it + /// does not turn the parent query into an inner join. Selection required({ToOneStrategy strategy = ToOneStrategy.automatic}) => _useJoin(strategy) ? _JoinedRelationSelection(this, required: true) @@ -122,58 +212,87 @@ final class Relation { } return rows.single; }); - Expr any() => Expr._(_RelationSubquery(this, false), Codecs.boolean); + + /// Builds a correlated SQL EXISTS expression for the filtered relationship. + /// + /// Apply it before [take] or [skip]; paginated relationships are rejected. + Expr any() => + Expr.internal(RelationSubqueryNode(this, false), Codecs.boolean); + + /// Builds the negation of [any], without loading the related rows. Expr none() => any().not(); + + /// Checks that every filtered related row makes [condition] SQL TRUE. + /// + /// An empty relationship satisfies this expression. SQL FALSE and SQL NULL + /// both count as failures of the condition. Apply it before [take] or [skip]. Expr every(Expr Function(F) condition) { - final conditionNode = condition(_fields)._node; + final conditionNode = condition(queryFields).expressionNode; return where( - (_) => Expr._( - _Unary('IS NOT TRUE', conditionNode, postfix: true), + (_) => Expr.internal( + UnaryNode('IS NOT TRUE', conditionNode, postfix: true), Codecs.boolean, ), ).none(); } - Expr count() => Expr._(_RelationSubquery(this, true), Codecs.integer); + /// Builds a correlated SQL count without fetching related rows into Dart. + /// + /// Counts filtered related rows; [take] and [skip] are not supported here. + Expr count() => + Expr.internal(RelationSubqueryNode(this, true), Codecs.integer); } final class _JoinedRelationSelection( final Relation relation, { final bool required = false, }) extends Selection { + /// @nodoc + @internal @override - _Decoder _bind(_SelectionPlan plan) { - Expr match(int i) => Expr._( - _Binary(relation._child[i]._node, '=', relation._parent[i]._node), + RowDecoder bindSelection(SelectionPlan plan) { + Expr match(int i) => Expr.internal( + BinaryNode( + relation.childFields[i].expressionNode, + '=', + relation.parentFields[i].expressionNode, + ), Codecs.boolean.nullable(), ); Expr predicate = match(0); - for (var i = 1; i < relation._child.length; i++) { + for (var i = 1; i < relation.childFields.length; i++) { predicate = predicate.and(match(i)); } - if (relation._state.predicate case final filter?) { + if (relation.queryState.predicate case final filter?) { predicate = predicate.and(filter); } // A proven unique match has at most one row, so skipping it or taking zero // must produce an absent relation, without filtering out the root row. - if (relation._state.limit == 0 || (relation._state.offset ?? 0) > 0) { + if (relation.queryState.limit == 0 || + (relation.queryState.offset ?? 0) > 0) { predicate = predicate.and(value(false, Codecs.boolean)); } final existing = plan.joins - .where((join) => join.alias.fields.table == relation._fields.table) + .where((join) => join.alias.fields.table == relation.queryFields.table) .firstOrNull; if (existing == null) { - plan.joins.add(_Join(relation._alias, predicate, true)); - } else if (!_sameSqlNode(existing.on._node, predicate._node)) { + plan.joins.add(Join(relation._alias, predicate, true)); + } else if (!sameSqlNode( + existing.on.expressionNode, + predicate.expressionNode, + )) { throw const OrmException( 'RELATION.ALIAS', 'Use a fresh relation getter for differently filtered joins.', ); } final marker = plan.column( - Expr._(_Presence(relation._alias), Codecs.integer.nullable()), + Expr.internal(PresenceNode(relation._alias), Codecs.integer.nullable()), + ); + final decode = plan.optional( + relation.queryFields.table, + relation.querySelection, ); - final decode = plan.optional(relation._fields.table, relation._selection); return (row) { if (row[marker] != null) return decode(row); if (required) { @@ -188,63 +307,27 @@ final class _JoinedRelationSelection( } } -final class _RelationSubquery( - final Relation relation, - final bool count, -) extends _Node { - @override - String writeSql(_Writer w) { - if (relation._state.limit != null || relation._state.offset != null) { - throw const OrmException( - 'RELATION.AGGREGATE', - 'Apply relation count/any/every before pagination.', - ); - } - final source = relation._state.source; - w.reads?.tables.add(source.schema); - final previous = w.aliases[source]; - final alias = 't${w.aliases.length}'; - w.aliases[source] = alias; - try { - final predicates = [ - for (var i = 0; i < relation._child.length; i++) - _Binary( - relation._child[i]._node, - '=', - relation._parent[i]._node, - ).write(w), - if (relation._state.predicate case final p?) p._node.write(w), - ]; - final query = - 'SELECT ${count ? 'COUNT(*)' : '1'} FROM ${w.quote(source.schema.name)} AS ${w.quote(alias)} WHERE ${predicates.join(' AND ')}'; - return count ? '($query)' : 'EXISTS ($query)'; - } finally { - if (previous == null) { - w.aliases.remove(source); - } else { - w.aliases[source] = previous; - } - } - } -} - final class _RelationSelection( final Relation relation, ) extends Selection> { + /// @nodoc + @internal @override - _Decoder> _bind(_SelectionPlan plan) { + RowDecoder> bindSelection(SelectionPlan plan) { final index = plan.relations.length; final parentIndices = [ - for (final key in relation._parent) plan.column(key), + for (final key in relation.parentFields) plan.column(key), ]; - plan.relations.add(_TypedRelationBinding(relation, parentIndices)); + plan.relations.add(TypedRelationBinding(relation, parentIndices)); return (row) => row[plan.columns.length + index] as List; } } -abstract class _RelationBinding { +/// @nodoc +@internal +abstract class RelationBinding { RelationLoadPlan inspect(QueryContext db); - void collectReads(QueryContext db, _ReadTables reads); + void collectReads(QueryContext db, ReadTables reads); Future> load( QueryContext db, SqlConnection connection, @@ -253,15 +336,22 @@ abstract class _RelationBinding { }); } -final class _TypedRelationBinding( +/// @nodoc +@internal +final class TypedRelationBinding( final Relation relation, final List parentIndices, -) extends _RelationBinding { +) extends RelationBinding { @override - RelationLoadPlan inspect(QueryContext db) => _inspectRelation(db, this); + RelationLoadPlan inspect(QueryContext db) => inspectRelation(db, this); @override - void collectReads(QueryContext db, _ReadTables reads) => reads.query( - Query._(db, relation._fields, relation._state, relation._selection), + void collectReads(QueryContext db, ReadTables reads) => reads.query( + Query.internal( + db, + relation.queryFields, + relation.queryState, + relation.querySelection, + ), ); @override @@ -271,30 +361,33 @@ final class _TypedRelationBinding( List> parents, { ExecutionOptions options = const ExecutionOptions(), }) async { - final keys = <_RelationKey>{}; + final keys = {}; final parentKeys = [ for (final row in parents) - _RelationKey([ + RelationKey([ for (var i = 0; i < parentIndices.length; i++) - _relationValue(relation._parent[i], row[parentIndices[i]]), + _relationValue(relation.parentFields[i], row[parentIndices[i]]), ]), ]; for (final key in parentKeys) { if (!key.values.contains(null)) keys.add(key); } - final grouped = <_RelationKey, List>{}; - if (keys.isEmpty || relation._state.limit == 0) { + final grouped = >{}; + if (keys.isEmpty || relation.queryState.limit == 0) { return [for (final _ in parents) []]; } - final plan = _SelectionPlan(); - final decode = relation._selection._bind(plan); - final childIndices = [for (final key in relation._child) plan.column(key)]; + final plan = SelectionPlan(); + final decode = relation.querySelection.bindSelection(plan); + final childIndices = [ + for (final key in relation.childFields) plan.column(key), + ]; final all = keys.toList(); final baseCount = - _compile(db, plan, [all.first]).parameters.length - - relation._child.length; + compileQuery(db, plan, [all.first]).parameters.length - + relation.childFields.length; final chunkSize = - (db.capabilities.maxParameters - baseCount) ~/ relation._child.length; + (db.capabilities.maxParameters - baseCount) ~/ + relation.childFields.length; if (chunkSize < 1) { throw const OrmException( 'QUERY.PARAMETERS', @@ -305,9 +398,9 @@ final class _TypedRelationBinding( final end = offset + chunkSize < all.length ? offset + chunkSize : all.length; - final command = _compile(db, plan, all.sublist(offset, end)); + final command = compileQuery(db, plan, all.sublist(offset, end)); final result = await db.executeOn(connection, command, options: options); - final rows = await _expandRelations( + final rows = await expandRelations( db, connection, plan, @@ -316,9 +409,9 @@ final class _TypedRelationBinding( ); db.observeDecode(command.sql, rows.length, () { for (final row in rows) { - final key = _RelationKey([ + final key = RelationKey([ for (var i = 0; i < childIndices.length; i++) - _relationValue(relation._child[i], row[childIndices[i]]), + _relationValue(relation.childFields[i], row[childIndices[i]]), ]); (grouped[key] ??= []).add(decode(row)); } @@ -329,14 +422,14 @@ final class _TypedRelationBinding( ]; } - SqlCommand _compile( + SqlCommand compileQuery( QueryContext db, - _SelectionPlan plan, - List<_RelationKey> keys, { - _ReadTables? reads, + SelectionPlan plan, + List keys, { + ReadTables? reads, }) { - final state = relation._state; - final w = _Writer( + final state = relation.queryState; + final w = SqlWriter( db.dialect, {}, database: db, @@ -344,13 +437,13 @@ final class _TypedRelationBinding( exactDecimal: db.capabilities.exactDecimal, temporal: db.capabilities.temporal, ); - Expr predicate = Expr._( - _RelationKeys(relation._child, keys), + Expr predicate = Expr.internal( + RelationKeys(relation.childFields, keys), Codecs.boolean, ); if (state.predicate case final filter?) predicate = predicate.and(filter); final paginated = state.limit != null || state.offset != null; - final sqlPlan = _SelectionPlan() + final sqlPlan = SelectionPlan() ..columns.addAll(plan.columns) ..required.addAll(plan.required) ..guarded.addAll(plan.guarded) @@ -363,20 +456,25 @@ final class _TypedRelationBinding( ); } sqlPlan.columns.add( - rowNumber(partitionBy: relation._child, orderBy: state.order), + rowNumber(partitionBy: relation.childFields, orderBy: state.order), ); } - final query = Query._( + final query = Query.internal( db, - relation._fields, - _QueryState( + relation.queryFields, + QueryState( state.source, predicate: predicate, order: paginated ? const [] : state.order, ), - relation._selection, + relation.querySelection, + ); + var text = query.writeQuery( + w, + sqlPlan, + aliasColumns: true, + decodeResult: true, ); - var text = query._write(w, sqlPlan, aliasColumns: true, decodeResult: true); if (paginated) { final offset = state.offset ?? 0; final rank = w.quote('c${plan.columns.length}'); @@ -392,10 +490,12 @@ final class _TypedRelationBinding( } } -Future>> _expandRelations( +/// @nodoc +@internal +Future>> expandRelations( QueryContext db, SqlConnection connection, - _SelectionPlan plan, + SelectionPlan plan, List> source, { ExecutionOptions options = const ExecutionOptions(), }) async { @@ -423,40 +523,15 @@ Future>> _expandRelations( return rows; } -// Row-value IN avoids a deep OR tree for large composite-key batches. -final class _RelationKeys( - final List> columns, - final List<_RelationKey> keys, -) extends _Node { - @override - String writeSql(_Writer w) { - // Keys stay comparable storage values while grouping. Restore floating SQL - // intent only when binding them (JS cannot identify an integral double). - Object? parameter(int index, Object? value) => - value != null && columns[index].codec.sqlType == 'real' - ? Codecs.real.encode(Codecs.real.decode(value)) - : value; - if (columns.length == 1) { - return _In(columns.single._node, [ - for (final key in keys) - _Parameter( - parameter(0, key.values.single), - storageType: columns.single.codec.sqlType, - ), - ]).write(w); - } - return '(${columns.map((c) => c._node.write(w)).join(', ')}) IN (${w.dialect == SqlDialect.sqlite ? 'VALUES ' : ''}' - '${keys.map((key) => '(${[for (var i = 0; i < key.values.length; i++) _Parameter(parameter(i, key.values[i]), storageType: columns[i].codec.sqlType).write(w)].join(', ')})').join(', ')})'; - } -} - -final class _RelationKey(final List values) { +/// @nodoc +@internal +final class RelationKey(final List values) { @override int get hashCode => Object.hashAll(values.map((v) => v is Uint8List ? Object.hashAll(v) : v)); @override bool operator ==(Object other) { - if (other is! _RelationKey || values.length != other.values.length) { + if (other is! RelationKey || values.length != other.values.length) { return false; } for (var i = 0; i < values.length; i++) { diff --git a/lib/src/selection.dart b/lib/src/query/selection.dart similarity index 57% rename from lib/src/selection.dart rename to lib/src/query/selection.dart index 76b5c5b0..443c5422 100644 --- a/lib/src/selection.dart +++ b/lib/src/query/selection.dart @@ -1,19 +1,42 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; -typedef _Decoder = T Function(List row); +import 'expression.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'relation.dart'; +import 'table.dart'; +/// @nodoc +@internal +typedef RowDecoder = T Function(List row); + +/// A typed result description decoded when its containing query executes. +/// +/// SQL expressions are scalar selections. Record composition and [map] combine +/// decoded values into application results without evaluating Dart code in SQL. abstract class Selection { + /// Base constructor for typed projections and their Dart result mappings. const Selection(); - _Decoder _bind(_SelectionPlan plan); + + /// @nodoc + @internal + RowDecoder bindSelection(SelectionPlan plan); + + /// Maps each decoded value in Dart, preserving the source SQL projection. + /// + /// The mapper runs during decoding, not during query construction. Its result + /// does not become a SQL expression for filtering, grouping, or set operations. Selection map(R Function(T) mapper) => _Mapped(this, mapper); } -final class _SelectionPlan { +/// @nodoc +@internal +final class SelectionPlan { final bool deduplicate; - _SelectionPlan({this.deduplicate = true}); + SelectionPlan({this.deduplicate = true}); final List> columns = []; - final List<_RelationBinding> relations = []; - final List<_Join> joins = []; + final List relations = []; + final List joins = []; final List> required = []; final Map, List>> guarded = {}; Set _present = const {}; @@ -26,23 +49,23 @@ final class _SelectionPlan { } } - _Decoder optional(TableRef table, Selection selection) { + RowDecoder optional(TableRef table, Selection selection) { final saved = _present; _present = {...saved, table}; try { - return selection._bind(this); + return selection.bindSelection(this); } finally { _present = saved; } } - final Map<_Node, int> _indices = {}; + final Map _indices = {}; int column(Expr expression) { if (!deduplicate) { columns.add(expression); return columns.length - 1; } - return _indices.putIfAbsent(expression._node, () { + return _indices.putIfAbsent(expression.expressionNode, () { columns.add(expression); return columns.length - 1; }); @@ -51,28 +74,34 @@ final class _SelectionPlan { final class _Mapped(final Selection source, final R Function(T) mapper) extends Selection { + /// @nodoc + @internal @override - _Decoder _bind(_SelectionPlan plan) { - final decode = source._bind(plan); + RowDecoder bindSelection(SelectionPlan plan) { + final decode = source.bindSelection(plan); return (row) => mapper(decode(row)); } } final class _Combined( final List> sources, - final _Decoder Function(List<_Decoder>) combine, + final RowDecoder Function(List>) combine, ) extends Selection { + /// @nodoc + @internal @override - _Decoder _bind(_SelectionPlan plan) => combine( + RowDecoder bindSelection(SelectionPlan plan) => combine( List.generate( sources.length, - (i) => sources[i]._bind(plan), + (i) => sources[i].bindSelection(plan), growable: false, ), ); } +/// Composes two independently typed selections into one decoded result. extension Selection2 on (Selection, Selection) { + /// Maps the two decoded values in Dart without changing their SQL meaning. Selection map(R Function(A, B) mapper) => _Combined( [$1, $2], (read) => @@ -80,7 +109,9 @@ extension Selection2 on (Selection, Selection) { ); } +/// Composes three independently typed selections into one decoded result. extension Selection3 on (Selection, Selection, Selection) { + /// Maps the three decoded values in Dart after the query executes. Selection map(R Function(A, B, C) mapper) => _Combined( [$1, $2, $3], (read) => @@ -89,8 +120,10 @@ extension Selection3 on (Selection, Selection, Selection) { ); } +/// Composes four independently typed selections into one decoded result. extension Selection4 on (Selection, Selection, Selection, Selection) { + /// Maps the four decoded values in Dart after the query executes. Selection map(R Function(A, B, C, D) mapper) => _Combined( [$1, $2, $3, $4], (read) => @@ -103,8 +136,10 @@ extension Selection4 ); } +/// Composes five independently typed selections into one decoded result. extension Selection5 on (Selection, Selection, Selection, Selection, Selection) { + /// Maps the five decoded values in Dart after the query executes. Selection map(R Function(A, B, C, D, E) mapper) => _Combined( [$1, $2, $3, $4, $5], (read) => @@ -118,6 +153,7 @@ extension Selection5 ); } +/// Composes six independently typed selections into one decoded result. extension Selection6 on ( @@ -128,6 +164,7 @@ extension Selection6 Selection, Selection, ) { + /// Maps the six decoded values in Dart after the query executes. Selection map(R Function(A, B, C, D, E, F) mapper) => _Combined( [$1, $2, $3, $4, $5, $6], (read) => @@ -143,6 +180,10 @@ extension Selection6 } /// Runtime field selection deliberately returns dynamic values. +/// +/// Captures the selected names and projections now and builds a map for each +/// decoded row. Use a scalar or Record selection when result fields are known +/// statically and their individual Dart types should be preserved. Selection> fields( Map> selected, ) { diff --git a/lib/src/query_stream.dart b/lib/src/query/stream.dart similarity index 59% rename from lib/src/query_stream.dart rename to lib/src/query/stream.dart index 3ec01ce1..c822e8da 100644 --- a/lib/src/query_stream.dart +++ b/lib/src/query/stream.dart @@ -1,21 +1,30 @@ -part of '../sql.dart'; +import '../../driver.dart'; +import 'query.dart'; +import 'relation.dart'; +import 'table.dart'; +/// Cursor-backed, demand-driven execution of a typed query. extension QueryStreaming on Query { /// One database cursor, fetched on demand. Related rows are loaded per root /// batch on the same connection. Pause/await-for consumption provides demand. + /// + /// SQL is compiled when this method is called; the single-subscription stream + /// acquires a connection when listened to. [batchSize] must be positive and + /// the driver must support streaming. Cancel or finish the subscription before + /// leaving an explicit session or transaction callback. Stream stream({ int batchSize = 128, ExecutionOptions options = const ExecutionOptions(), }) { if (batchSize < 1) throw ArgumentError.value(batchSize, 'batchSize'); - final (plan, decode) = _plan(); - final command = _compile(plan); + final (plan, decode) = planQuery(); + final command = compileQuery(plan); return database.streamRows( command, batchSize: batchSize, options: options, decode: (connection, rows, execution) async { - final expanded = await _expandRelations( + final expanded = await expandRelations( database, connection, plan, diff --git a/lib/src/query/table.dart b/lib/src/query/table.dart new file mode 100644 index 00000000..277f2a62 --- /dev/null +++ b/lib/src/query/table.dart @@ -0,0 +1,165 @@ +import 'package:meta/meta.dart'; + +import '../../schema_model.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'mutation.dart'; +import 'nodes.dart'; +import 'selection.dart'; + +/// A table occurrence has nominal identity, even when two row records have the +/// same structural Dart type or a query joins the same physical table twice. +final class TableRef { + /// The physical schema for this occurrence of a table. + final TableSchema schema; + + /// Creates a distinct occurrence, even when the schema is shared. + TableRef(this.schema); +} + +/// Typed expressions belonging to one table occurrence. +/// +/// Generated field classes extend this type. Manual definitions use [column] for +/// writable columns and [readColumn] for database-computed columns. +abstract class Fields { + /// The table occurrence that owns these expressions. + final TableRef table; + + /// Binds a field collection to one table occurrence. + const Fields(this.table); + + /// Creates a writable field; rejects computed column definitions. + Field column(Column column) => Field.internal(table, column); + + /// Creates a read-only field, including a database-computed column. + ReadField readColumn(Column column) => + ReadField.internal(table, column); +} + +/// A selectable column expression with no assignment operations. +base class ReadField extends Expr { + /// The owning table occurrence, used to validate query scope. + final TableRef table; + + /// The column's name, codec and physical constraints. + final Column definition; + + /// @nodoc + @internal + ReadField.internal(this.table, this.definition) + : super.internal(ColumnNode(table, definition.name), definition.codec); +} + +/// A writable column expression that can create mutation assignments. +final class Field extends ReadField { + /// @nodoc + @internal + Field.internal(super.table, super.definition) : super.internal() { + if (definition.computed != null) { + throw const OrmException( + 'COLUMN.READ_ONLY', + 'Use readColumn for a computed column.', + ); + } + } + + /// Assigns a bound value, including NULL when the field is nullable. + Assignment set(T value) => + _assign(ParameterNode(codec.encode(value), storageType: codec.sqlType)); + + /// Assigns a SQL expression with the same Dart value type. + Assignment setExpression(Expr expression) => + _assign(expression.expressionNode); + Assignment _assign(SqlNode node) => Assignment.internal( + this, + definition.decimalPrecision == null + ? definition.temporalPrecision == null + ? node + : TemporalCast( + node, + definition.codec.sqlType, + definition.temporalPrecision!, + columnAssignment: true, + ) + : DecimalCast( + node, + definition.decimalPrecision!, + definition.decimalScale ?? 0, + columnAssignment: true, + ), + ); + + /// Requests the database default rather than a Dart client default. + Assignment defaultValue() => Assignment.internal(this, null); +} + +/// Atomic numeric updates evaluated inside the database statement. +extension NumericField on Field { + /// Adds a bound amount to the stored value. + Assignment increment(T amount) => setExpression(plus(amount)); + + /// Subtracts a bound amount from the stored value. + Assignment decrement(T amount) => setExpression(minus(amount)); +} + +/// A table's physical schema, typed fields and full-row decoder. +/// +/// Use generated definitions for normal application code, or construct one for a +/// manual schema. A definition can be bound to different database sessions. +final class Table { + /// The physical schema used for validation and SQL generation. + final TableSchema schema; + + /// Creates typed fields for a fresh table occurrence. + final F Function(TableRef) createFields; + + /// Builds the full-row selection from that occurrence's fields. + final Selection Function(F) selectRow; + + /// Defines a table without opening a database connection. + const Table(this.schema, this.createFields, this.selectRow); + + /// Creates an independent alias for a join or self-join. + TableAlias alias() => TableAlias.internal(this); +} + +/// A patch value that distinguishes omission, assignment and SQL DEFAULT. +/// +/// Use `.keep()` to omit, `.set(value)` to write, or `.defaultValue()` to request +/// the database default. Setting null is legal only for a nullable field. +sealed class Change { + /// Base constructor for the closed set of patch operations. + const Change(); + + /// Leaves the field out of the update. + const factory Change.keep() = _Keep; + + /// Writes the supplied value, including an explicitly allowed null. + const factory Change.set(T value) = _Set; + + /// Requests the database default for this field. + const factory Change.defaultValue() = _Default; +} + +final class _Keep extends Change { + const _Keep(); +} + +final class _Set extends Change { + final T value; + const _Set(this.value); +} + +final class _Default extends Change { + const _Default(); +} + +/// Converts a generated patch input into zero or one SQL assignment. +extension ChangeField on Field { + /// Omits unchanged fields and builds assignments for the other cases. + List change(Change change) => switch (change) { + _Keep() => [], + _Set(:final value) => [set(value)], + _Default() => [defaultValue()], + }; +} diff --git a/lib/src/union.dart b/lib/src/query/union.dart similarity index 56% rename from lib/src/union.dart rename to lib/src/query/union.dart index efe2d5e0..74589304 100644 --- a/lib/src/union.dart +++ b/lib/src/query/union.dart @@ -1,4 +1,13 @@ -part of '../sql.dart'; +import 'package:meta/meta.dart'; + +import '../../schema_model.dart'; +import 'cte.dart'; +import 'expression.dart'; +import 'joins.dart'; +import 'nodes.dart'; +import 'query.dart'; +import 'selection.dart'; +import 'table.dart'; /// SQL records have no Dart mapping step. Set operations compare their stored /// columns before any subsequent Record/DTO mapping. @@ -6,23 +15,31 @@ final class _SqlRow( final List> columns, final R Function(List) assemble, ) extends Selection { + /// @nodoc + @internal @override - _Decoder _bind(_SelectionPlan plan) { - final readers = [for (final column in columns) column._bind(plan)]; + RowDecoder bindSelection(SelectionPlan plan) { + final readers = [for (final column in columns) column.bindSelection(plan)]; return (row) => assemble([for (final read in readers) read(row)]); } } +/// Projects two SQL expressions as a positional Dart Record. extension SqlRow2 on (Expr, Expr) { + /// Keeps both SQL columns available for DISTINCT, CTEs, and set operations. Selection<(A, B)> get row => _SqlRow([$1, $2], (v) => (v[0] as A, v[1] as B)); } +/// Projects three SQL expressions as a positional Dart Record. extension SqlRow3 on (Expr, Expr, Expr) { + /// Preserves column positions and individual nullability in the result. Selection<(A, B, C)> get row => _SqlRow([$1, $2, $3], (v) => (v[0] as A, v[1] as B, v[2] as C)); } +/// Projects four SQL expressions as a positional Dart Record. extension SqlRow4 on (Expr, Expr, Expr, Expr) { + /// Preserves column positions and individual nullability in the result. Selection<(A, B, C, D)> get row => _SqlRow([ $1, $2, @@ -31,8 +48,10 @@ extension SqlRow4 on (Expr, Expr, Expr, Expr) { ], (v) => (v[0] as A, v[1] as B, v[2] as C, v[3] as D)); } +/// Projects five SQL expressions as a positional Dart Record. extension SqlRow5 on (Expr, Expr, Expr, Expr, Expr) { + /// Preserves column positions and individual nullability in the result. Selection<(A, B, C, D, E)> get row => _SqlRow([ $1, $2, @@ -42,8 +61,10 @@ extension SqlRow5 ], (v) => (v[0] as A, v[1] as B, v[2] as C, v[3] as D, v[4] as E)); } +/// Projects six SQL expressions as a positional Dart Record. extension SqlRow6 on (Expr, Expr, Expr, Expr, Expr, Expr) { + /// Preserves column positions and individual nullability in the result. Selection<(A, B, C, D, E, F)> get row => _SqlRow([ $1, $2, @@ -54,13 +75,24 @@ extension SqlRow6 ], (v) => (v[0] as A, v[1] as B, v[2] as C, v[3] as D, v[4] as E, v[5] as F)); } +/// SQL set operations over matching scalar or positional Record projections. +/// +/// Both operands must belong to the same database or transaction context. Apply +/// Dart mapping after combining SQL rows; a DTO mapper does not define SQL set +/// equality. Constructing a set query performs no database I/O. extension SetQueries on Query { /// Combines SQL scalars or `.row` projections. Both operands must use this /// database/session and the same codecs, column types and nullability. + /// + /// SQL UNION removes duplicate projected rows. Declare ordering on the + /// resulting query when result order matters. Query> union(Query other) => _union(other, all: false); /// Keeps duplicate SQL rows. Row order requires an explicit final orderBy. + /// + /// The same column, codec, nullability, and context requirements as [union] + /// apply; this describes one SQL set operation, not two Dart list reads. Query> unionAll(Query other) => _union(other, all: true); @@ -74,15 +106,15 @@ extension SetQueries on Query { 'UNION operands must use the same database/session.', ); } - final shape = _sqlRowShape(_selection); - if (shape == null || shape != _sqlRowShape(other._selection)) { + final shape = sqlRowShape(querySelection); + if (shape == null || shape != sqlRowShape(other.querySelection)) { throw const OrmException( 'QUERY.UNION_SELECTION', 'Select SQL expressions or matching .row records. Map after UNION.', ); } // SQL set columns are positional: selecting (id, id) still exports two. - final (left, decode) = _plan(deduplicate: false); + final (left, decode) = planQuery(deduplicate: false); if ((database.dialect == SqlDialect.mysql || database.dialect == SqlDialect.mariadb) && left.columns.any((e) => e.codec.sqlType == 'decimal')) { @@ -91,7 +123,7 @@ extension SetQueries on Query { 'MySQL/MariaDB decimal UNION may silently narrow mixed precision; use explicit native SQL when its precision is acceptable.', ); } - final (right, _) = other._plan(deduplicate: false); + final (right, _) = other.planQuery(deduplicate: false); if (left.columns.length != right.columns.length || left.relations.isNotEmpty || right.relations.isNotEmpty) { @@ -123,41 +155,60 @@ extension SetQueries on Query { ], ), ); - final fields = UnionFields._(table, _fields, left); + final fields = UnionFields.internal(table, queryFields, left); final columns = [ for (var i = 0; i < left.columns.length; i++) - Expr._(_ColumnNode(table, 'c$i'), left.columns[i].codec), + Expr.internal(ColumnNode(table, 'c$i'), left.columns[i].codec), ]; // A scalar remains an Expr, so scalar()/isInQuery() compose unchanged. final Selection selection = shape == 0 - ? Expr._(columns.single._node, left.columns.single.codec as Codec) - : _ReboundSelection(decode, columns, source: _selection); - return Query._( + ? Expr.internal( + columns.single.expressionNode, + left.columns.single.codec as Codec, + ) + : ReboundSelection(decode, columns, source: querySelection); + return Query.internal( database, fields, - _QueryState(table, union: _UnionSource(this, left, other, right, all)), + QueryState(table, union: UnionSource(this, left, other, right, all)), selection, ); } } -int? _sqlRowShape(Selection selection) => switch (selection) { +/// @nodoc +@internal +int? sqlRowShape(Selection selection) => switch (selection) { Expr() => 0, _SqlRow(:final columns) => columns.length, - _ReboundSelection(:final source?) => _sqlRowShape(source), + ReboundSelection(:final source?) => sqlRowShape(source), _ => null, }; /// References refer to the left operand's exported SQL expressions. +/// +/// Use [ref] when filtering, selecting, or ordering the result of a set operation. final class UnionFields extends Fields { - final F _original; - final _SelectionPlan _plan; - UnionFields._(super.table, this._original, this._plan); + /// @nodoc + @internal + final F sourceFields; + + /// @nodoc + @internal + final SelectionPlan planQuery; + + /// @nodoc + @internal + UnionFields.internal(super.table, this.sourceFields, this.planQuery); + /// References a selected left-operand expression in the combined SQL result. + /// + /// The expression must match an exported column with its original codec and + /// nullability. Expressions omitted by the operand's selection are rejected. Expr ref(Expr Function(F) expression) { - final original = expression(_original); - final index = _plan.columns.indexWhere( - (e) => _sameSqlNode(e._node, original._node), + final original = expression(sourceFields); + final index = planQuery.columns.indexWhere( + (e) => sameSqlNode(e.expressionNode, original.expressionNode), ); if (index < 0) { throw const OrmException( @@ -165,31 +216,34 @@ final class UnionFields extends Fields { 'UNION did not export this SQL expression.', ); } - if (_plan.columns[index].codec.acceptsNull && !original.codec.acceptsNull) { + if (planQuery.columns[index].codec.acceptsNull && + !original.codec.acceptsNull) { throw const OrmException( 'QUERY.NULLABILITY', 'Reference this UNION column with a nullable expression.', ); } - if (!_plan.columns[index].codec.sameStorageAs(original.codec)) { + if (!planQuery.columns[index].codec.sameStorageAs(original.codec)) { throw const OrmException( 'QUERY.UNION_CODEC', 'Reference the exported UNION codec.', ); } - return Expr._(_ColumnNode(table, 'c$index'), original.codec); + return Expr.internal(ColumnNode(table, 'c$index'), original.codec); } } -final class _UnionSource( +/// @nodoc +@internal +final class UnionSource( final Query left, - final _SelectionPlan leftPlan, + final SelectionPlan leftPlan, final Query right, - final _SelectionPlan rightPlan, + final SelectionPlan rightPlan, final bool all, ) { - String write(_Writer w) => - 'SELECT * FROM (${left._write(w, leftPlan, aliasColumns: true)}) AS ${w.quote('_union_left')} ' + String write(SqlWriter w) => + 'SELECT * FROM (${left.writeQuery(w, leftPlan, aliasColumns: true)}) AS ${w.quote('_union_left')} ' 'UNION${all ? ' ALL' : ''} ' - 'SELECT * FROM (${right._write(w, rightPlan, aliasColumns: true)}) AS ${w.quote('_union_right')}'; + 'SELECT * FROM (${right.writeQuery(w, rightPlan, aliasColumns: true)}) AS ${w.quote('_union_right')}'; } diff --git a/lib/src/execution.dart b/lib/src/runtime/acquisition.dart similarity index 93% rename from lib/src/execution.dart rename to lib/src/runtime/acquisition.dart index 80bbb031..8a305c37 100644 --- a/lib/src/execution.dart +++ b/lib/src/runtime/acquisition.dart @@ -1,9 +1,15 @@ -part of '../runtime.dart'; +import 'dart:async'; + +import 'package:meta/meta.dart' show internal; + +import '../../driver.dart'; /// The driver may not expose a cancellable queue. Abandon the request, never /// enter its SQL callback, and release a late lease normally. The separately /// tracked drain future keeps SqlDatabase.close() aware of outstanding resources. -final class _ConnectionWait { +/// @nodoc +@internal +final class ConnectionWait { final Completer _result = Completer(); Timer? _timer; final Stopwatch? _clock; @@ -14,7 +20,7 @@ final class _ConnectionWait { late final Future drained; Future get result => _result.future; - _ConnectionWait( + ConnectionWait( Driver driver, Future Function(SqlConnection) action, AcquisitionOptions options, { diff --git a/lib/src/runtime/database.dart b/lib/src/runtime/database.dart index 94e43bc8..21f67aa7 100644 --- a/lib/src/runtime/database.dart +++ b/lib/src/runtime/database.dart @@ -1,14 +1,43 @@ -part of '../../runtime.dart'; +import 'dart:async'; -/// SqlDatabase owns its driver. A transaction view borrows one connection and -/// becomes unusable as soon as its callback finishes. +import 'package:meta/meta.dart' show internal; + +import '../../driver.dart'; +import 'acquisition.dart'; +import 'events.dart'; +import 'mysql_transaction.dart'; +import 'options.dart'; +import 'transaction.dart'; + +/// Raw SQL execution with owned driver resources and explicit callback scopes. +/// +/// The root database owns its [driver]. Session and transaction callbacks receive +/// borrowed views that expire when their callback finishes. Await every operation +/// before leaving those scopes; active cursors and unfinished work are rejected. +/// +/// ```dart +/// Future renameUser(SqlDatabase db, int id, String name) async { +/// await db.transaction((tx) async { +/// await tx.execute(SqlCommand( +/// 'UPDATE users SET name = ?1 WHERE id = ?2', [name, id], +/// )); +/// }); +/// } +/// ``` +/// +/// {@category Execution} class SqlDatabase { + /// Driver owned by the root database and shared with its borrowed views. final Driver driver; + + /// Optional observer for statements and cursor operations; errors are isolated. final void Function(QueryEvent)? onQuery; + + /// Optional observer for connection acquisition and existing-lease reuse. final void Function(AcquisitionEvent)? onAcquire; final SqlConnection? _connection; final bool _transaction; - final _TransactionControl? _control; + final TransactionControl? _control; final Set> _pending = {}; final Set Function()> _streams = {}; final Set Function()> _cursors = {}; @@ -31,6 +60,8 @@ class SqlDatabase { /// Listen for explicitly reported physical table changes after commit. /// Raw SQL is never parsed to guess the tables it changes. + /// @nodoc + @internal void Function() addChangeListener(void Function(Set) listener) { checkActive(); _changeListeners.add(listener); @@ -57,6 +88,8 @@ class SqlDatabase { /// Schedule cache invalidation after the transaction finishes. Rollback drops /// these callbacks; an uncertain COMMIT runs them conservatively. Savepoint /// callbacks join the parent transaction only after successful release. + /// @nodoc + @internal void deferInvalidation(void Function() action) { if (inTransaction) { _invalidations.add(action); @@ -67,6 +100,8 @@ class SqlDatabase { /// Register owned resources to stop before pending SQL is drained on close. /// Returns a function that removes this listener. + /// @nodoc + @internal void Function() addCloseListener(Future Function() listener) { checkActive(); if (inSession) { @@ -95,10 +130,13 @@ class SqlDatabase { /// Mark this transaction as failed when a multi-statement operation stops /// between statements. Catching its exception cannot commit a partial batch. + /// @nodoc + @internal void markFailed() { if (inTransaction) _statementFailed = true; } + /// Takes ownership of [driver]; close the root database to release it. SqlDatabase(this.driver, {this.onQuery, this.onAcquire}) : _changeListeners = {}, _connection = null, @@ -113,11 +151,21 @@ class SqlDatabase { this._control, required this._changeListeners, }); + + /// Features and limits exposed by the actual driver and platform. Capabilities get capabilities => driver.capabilities; + + /// Connected engine used for SQL syntax and transaction validation. SqlDialect get dialect => capabilities.dialect; + + /// Whether this view belongs to a transaction or savepoint callback. bool get inTransaction => _transaction; + + /// Whether this view already holds a connection lease. bool get inSession => _connection != null; + /// @nodoc + @internal void checkActive() { if (!_active) { throw const OrmException( @@ -141,6 +189,10 @@ class SqlDatabase { } } + /// Runs [action] with an acquired or already leased connection. + /// + /// This does not start a transaction. [acquire] limits only connection waiting; + /// the connection must not escape [action]. Prefer [execute] for one command. Future run( Future Function(SqlConnection) action, { AcquisitionOptions acquire = const AcquisitionOptions(), @@ -184,7 +236,7 @@ class SqlDatabase { final callback = observer == null ? action : enter; if (connection == null && (acquire.timeout != null || acquire.cancellation != null)) { - final wait = _ConnectionWait( + final wait = ConnectionWait( driver, callback, acquire, @@ -228,6 +280,8 @@ class SqlDatabase { return _SessionConnection(_connection, this); } + /// @nodoc + @internal Future executeOn( SqlConnection connection, SqlCommand command, { @@ -293,6 +347,11 @@ class SqlDatabase { } } + /// Executes bound SQL and returns raw rows and affected-row metadata. + /// + /// [changedTables] explicitly reports writes for query invalidation; SQL text + /// is never parsed to infer write targets. A transaction publishes these changes + /// after commit and discards them after a confirmed rollback. Future execute( SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), @@ -321,6 +380,10 @@ class SqlDatabase { /// Retains one connection across transactions and session-scoped operations. /// The borrowed view expires when the callback returns. + /// + /// A session does not begin a transaction. Use its [transaction] method for + /// atomic work; nested sessions are rejected. Returning with pending work or + /// an active stream fails and drains the borrowed view before lease release. Future session( Future Function(SqlDatabase session) action, { AcquisitionOptions acquire = const AcquisitionOptions(), @@ -363,6 +426,10 @@ class SqlDatabase { } /// Discards a leased connection after its state can no longer be recovered. + /// + /// Only a borrowed session or transaction view can be discarded. The view + /// becomes unusable immediately; this does not prove a submitted write rolled + /// back and does not close an otherwise usable root driver or pool. Future discard() async { final connection = _connection; if (connection == null) { @@ -375,6 +442,16 @@ class SqlDatabase { await connection.invalidate(); } + /// Runs [action] atomically and returns its result after confirmed commit. + /// + /// A failed statement marks the transaction failed even if its exception is + /// caught by application code. Use [savepoint] for recoverable work. Returning + /// with pending operations fails; borrowed transaction views cannot be reused. + /// + /// [timeout] starts after connection acquisition. [retry] has its own total + /// budget including acquisition and requires a safely repeatable callback. + /// Deadlines, cancellation, and retries require actual driver cancellation. + /// An uncertain COMMIT is reported and never replays the callback. Future transaction( Future Function(SqlDatabase tx) action, { TransactionOptions? options, @@ -385,7 +462,7 @@ class SqlDatabase { }) { checkActive(); acquire.check(); - retry?._check(); + retry?.validate(); if (options != null && options.dialect != dialect) { throw const OrmException( 'TRANSACTION.OPTIONS', @@ -415,9 +492,9 @@ class SqlDatabase { ); } final link = cancellation != null && acquire.cancellation != null - ? _CancellationLink([cancellation, acquire.cancellation!]) + ? CancellationLink([cancellation, acquire.cancellation!]) : null; - final budget = retry == null ? null : _RetryBudget(retry); + final budget = retry == null ? null : RetryBudget(retry); try { final acquireTimeout = budget?.limit(acquire.timeout) ?? acquire.timeout; final totalLimitsAcquisition = @@ -428,7 +505,7 @@ class SqlDatabase { final executionTimeout = budget?.limit(timeout) ?? timeout; final control = executionTimeout == null && cancellation == null ? null - : _TransactionControl(executionTimeout, cancellation); + : TransactionControl(executionTimeout, cancellation); if (_connection != null) _childActive = true; try { while (true) { @@ -442,7 +519,7 @@ class SqlDatabase { options, action, ); - } on _RetryAfterRollback catch (failure) { + } on RetryAfterRollback catch (failure) { if (await budget!.next(control!)) continue; Error.throwWithStackTrace(failure.error, failure.stack); } @@ -471,14 +548,14 @@ class SqlDatabase { Future _transactionAttempt( SqlConnection connection, - _TransactionControl? control, - _RetryBudget? budget, + TransactionControl? control, + RetryBudget? budget, TransactionOptions? options, Future Function(SqlDatabase) action, ) async { final scoped = control == null ? connection - : _TransactionConnection(connection, control); + : TransactionConnection(connection, control); final tx = SqlDatabase._( driver, scoped, @@ -587,7 +664,7 @@ class SqlDatabase { error is SqlFailure && error.retryTransaction && budget != null) { - throw _RetryAfterRollback(error, stack); + throw RetryAfterRollback(error, stack); } Error.throwWithStackTrace( expired == null || identical(expired, error) @@ -617,6 +694,11 @@ class SqlDatabase { return error; } + /// Runs nested work inside the current transaction using a database savepoint. + /// + /// Use the child view exclusively until [action] finishes. Success releases the + /// savepoint; failure rolls back the child work. A failed rollback invalidates + /// the connection instead of allowing the parent to continue in uncertain state. Future savepoint(Future Function(SqlDatabase tx) action) { if (!inTransaction) { throw const OrmException( @@ -666,7 +748,7 @@ class SqlDatabase { return result; } catch (error, stack) { final cleanup = await child._drain(); - final raw = _unscoped(scoped); + final raw = unscopedConnection(scoped); try { if (cleanup != null) throw cleanup; if (raw.transactionActive == false) { @@ -689,6 +771,10 @@ class SqlDatabase { }); } + /// Stops owned streams and watches, drains pending work, then closes the driver. + /// + /// Only the root database can close its driver. New operations are rejected + /// once closing starts; concurrent close calls await the same cleanup. Future close() async { if (_connection != null) { throw const OrmException( @@ -719,3 +805,323 @@ class SqlDatabase { } } } + +/// A session's public execution port. The underlying physical lease and its +/// transaction controls stay private to the runtime. +final class _SessionConnection( + final SqlConnection inner, + final SqlDatabase owner, +) implements SqlConnection { + @override + bool? get transactionActive => inner.transactionActive; + + Future _run(Future Function() action, {String? sql}) { + final result = Future.sync(() async { + owner.checkActive(); + try { + if (sql != null && + owner.inTransaction && + (owner.dialect == SqlDialect.mysql || + owner.dialect == SqlDialect.mariadb)) { + checkMysqlTransactionSql(sql); + } + return await action(); + } catch (_) { + owner.markFailed(); + rethrow; + } + }); + owner._track( + result.then((_) {}, onError: (Object _, StackTrace _) {}), + ); + return result; + } + + @override + Future execute( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }) => _run(() => inner.execute(command, options: options), sql: command.sql); + + @override + Future openCursor( + SqlCommand command, { + ExecutionOptions options = const ExecutionOptions(), + }) => _run(() async { + final cursor = _SessionCursor( + await inner.openCursor(command, options: options), + this, + ); + owner._cursors.add(cursor.close); + return cursor; + }, sql: command.sql); + + @override + Future invalidate() { + owner.checkActive(); + owner.markFailed(); + return inner.invalidate(); + } +} + +final class _SessionCursor( + final SqlCursor inner, + final _SessionConnection connection, +) implements SqlCursor { + Future? _closing; + @override + Future fetch( + int count, { + ExecutionOptions options = const ExecutionOptions(), + }) => connection._run(() { + if (_closing != null) { + throw const OrmException('CURSOR.CLOSED', 'Cursor has ended.'); + } + return inner.fetch(count, options: options); + }); + // Cleanup is allowed after the callback/deadline ends and runs only once. + @override + Future close() => _closing ??= _close(); + Future _close() async { + try { + await inner.close(); + } catch (_) { + connection.owner.markFailed(); + rethrow; + } finally { + connection.owner._cursors.remove(close); + } + } +} + +SqlConnection _physicalConnection(SqlConnection connection) => + switch (connection) { + _SessionConnection() => _physicalConnection(connection.inner), + TransactionConnection() => _physicalConnection(connection.inner), + _ => connection, + }; + +/// Demand-driven raw-row streaming with a bounded database cursor. +/// +/// {@category Execution} +extension SqlDatabaseStreaming on SqlDatabase { + /// Streams raw rows, fetching at most [batchSize] rows per database request. + /// + /// Listening holds a connection until completion or subscription cancellation. + /// Outside a transaction, the stream owns a read transaction and rolls it back + /// on early exit. Pausing the subscription prevents the next batch fetch; an + /// asynchronous `listen` callback must explicitly pause for demand control. + Stream> stream( + SqlCommand command, { + int batchSize = 128, + ExecutionOptions options = const ExecutionOptions(), + }) => streamRows( + command, + batchSize: batchSize, + options: options, + decode: (_, rows, _) async => rows, + ); + + /// @nodoc + @internal + Stream streamRows( + SqlCommand command, { + required int batchSize, + required ExecutionOptions options, + required Future> Function( + SqlConnection, + List>, + ExecutionOptions, + ) + decode, + }) { + if (batchSize < 1) throw ArgumentError.value(batchSize, 'batchSize'); + if (!capabilities.streaming) { + throw const OrmException( + 'CAPABILITY.STREAM', + 'This driver does not support database cursors.', + ); + } + if ((options.cancellation != null || options.timeout != null) && + !capabilities.cancellation) { + throw const OrmException( + 'CAPABILITY.CANCEL', + 'This driver cannot cancel a running statement.', + ); + } + options.check(); + if (command.parameters.length > capabilities.maxParameters) { + throw const OrmException( + 'QUERY.PARAMETERS', + 'Query exceeds the driver parameter limit.', + ); + } + final cancellation = CancellationToken(); + final execution = ExecutionOptions( + cancellation: capabilities.cancellation ? cancellation : null, + timeout: options.timeout, + ); + final finished = Completer(); + Completer? demand; + var stopped = false, started = false; + Object? cleanupFailure, requestedError; + late StreamController controller; + void requestStop() { + stopped = true; + cancellation.cancel(); + demand?.complete(); + demand = null; + } + + Future stop() async { + requestStop(); + if (started) await finished.future; + if (cleanupFailure != null) throw cleanupFailure!; + } + + Future waitForDemand() async { + if (controller.isPaused && !stopped) { + demand ??= Completer(); + await demand!.future; + } + } + + Future observe( + QueryOperation operation, + Future Function() action, + ) async { + final watch = onQuery == null ? null : (Stopwatch()..start()); + Object? error; + T? result; + try { + return result = await action(); + } catch (e) { + error = e; + if (inTransaction) _statementFailed = true; + rethrow; + } finally { + watch?.stop(); + try { + onQuery?.call( + QueryEvent( + operation: operation, + sql: command.sql, + parameterCount: operation == .cursorOpen + ? command.parameters.length + : 0, + elapsed: watch!.elapsed, + rowCount: result is SqlResult ? result.rows.length : null, + error: error, + ), + ); + } catch (_) {} + } + } + + Future produce() async { + started = true; + _streams.add(stop); + final unsubscribe = options.cancellation?.listen(() { + requestedError = const OrmException( + 'OPERATION.CANCELLED', + 'Stream cancelled.', + ); + requestStop(); + }); + try { + await run( + (connection) async { + final ownsTransaction = !inTransaction; + SqlCursor? cursor; + var complete = false, began = false; + try { + if (stopped) return; + if (ownsTransaction) { + await _executeOn( + connection is _SessionConnection + ? connection.inner + : connection, + SqlCommand(switch (dialect) { + SqlDialect.postgres => 'BEGIN READ ONLY', + SqlDialect.mysql || + SqlDialect.mariadb => 'START TRANSACTION READ ONLY', + SqlDialect.sqlite => 'BEGIN', + }), + ); + began = true; + } + cursor = await observe( + .cursorOpen, + () => connection.openCursor(command, options: execution), + ); + while (!stopped) { + await waitForDemand(); + if (stopped) break; + final batch = await observe( + .cursorFetch, + () => cursor!.fetch(batchSize, options: execution), + ); + if (stopped) break; + final rows = await decode(connection, batch.rows, execution); + for (final row in rows) { + await waitForDemand(); + if (stopped) break; + controller.add(row); + } + if (batch.rows.length < batchSize) { + complete = !stopped; + break; + } + } + } finally { + try { + if (cursor != null) await observe(.cursorClose, cursor.close); + if (began) { + await _executeOn( + connection is _SessionConnection + ? connection.inner + : connection, + SqlCommand(complete ? 'COMMIT' : 'ROLLBACK'), + ); + } + } catch (e) { + cleanupFailure = e; + if (inTransaction) _active = false; + await connection.invalidate(); + rethrow; + } + } + }, + acquire: AcquisitionOptions( + timeout: options.acquireTimeout, + cancellation: cancellation, + ), + ); + } catch (error, stack) { + if (!stopped) controller.addError(error, stack); + } finally { + unsubscribe?.call(); + if (requestedError != null) controller.addError(requestedError!); + _streams.remove(stop); + finished.complete(); + // A cancellation cleanup error is already exposed by stop() or the + // error event. Do not report it again through an unobserved close future. + unawaited(controller.close().catchError((Object _) {})); + } + } + + controller = StreamController( + sync: true, + onListen: () => unawaited(produce()), + onPause: () { + demand ??= Completer(); + }, + onResume: () { + demand?.complete(); + demand = null; + }, + onCancel: stop, + ); + return controller.stream; + } +} diff --git a/lib/src/runtime/events.dart b/lib/src/runtime/events.dart new file mode 100644 index 00000000..8c16df86 --- /dev/null +++ b/lib/src/runtime/events.dart @@ -0,0 +1,72 @@ +/// The driver operation measured by a [QueryEvent]. +enum QueryOperation { + /// A complete non-streaming statement, including transaction control SQL. + execute, + + /// Opening a database cursor before rows are fetched. + cursorOpen, + + /// Fetching the next bounded batch from an open cursor. + cursorFetch, + + /// Releasing a cursor and its database resources. + cursorClose, +} + +/// Timing and outcome of one SQL statement or cursor operation. +/// +/// Parameter values are deliberately absent. Observer exceptions are isolated +/// from the operation and cannot turn a successful commit into a failed result. +/// +/// {@category Observability} +final class QueryEvent { + /// Kind of statement or cursor operation measured. + final QueryOperation operation; + + /// Parameterized command text; cursor events retain their source query SQL. + final String sql; + + /// Bound value count; cursor fetch and close operations report zero. + final int parameterCount; + + /// Elapsed driver execution time, excluding connection acquisition. + final Duration elapsed; + + /// Number of returned rows, or null if no result was produced. + final int? rowCount; + + /// Failure raised by the operation, or null on success. + final Object? error; + + /// Records a measured operation without retaining bound parameter values. + const QueryEvent({ + this.operation = QueryOperation.execute, + required this.sql, + required this.parameterCount, + required this.elapsed, + this.rowCount, + this.error, + }); +} + +/// Time until a driver lease is granted or acquisition fails. Includes native +/// pool wait/connection setup; it does not separate those driver internals. +/// +/// {@category Observability} +final class AcquisitionEvent { + /// Time to obtain a driver lease; zero for an already leased connection. + final Duration elapsed; + + /// Whether execution reused a connection belonging to the current session. + final bool reusedConnection; + + /// Acquisition failure, or null when the callback receives its connection. + final Object? error; + + /// Records connection-wait timing independently of SQL execution timing. + const AcquisitionEvent({ + required this.elapsed, + required this.reusedConnection, + this.error, + }); +} diff --git a/lib/src/runtime/mysql_transaction.dart b/lib/src/runtime/mysql_transaction.dart index 2b2079c6..5892ae01 100644 --- a/lib/src/runtime/mysql_transaction.dart +++ b/lib/src/runtime/mysql_transaction.dart @@ -1,6 +1,10 @@ -part of '../../runtime.dart'; +import 'package:meta/meta.dart' show internal; -void _checkMysqlTransactionSql(String sql) { +import '../../driver.dart'; + +/// @nodoc +@internal +void checkMysqlTransactionSql(String sql) { Never reject() => throw const OrmException( 'TRANSACTION.STATEMENT', 'MySQL/MariaDB transactions allow one SELECT, INSERT, UPDATE, DELETE, ' diff --git a/lib/src/runtime/options.dart b/lib/src/runtime/options.dart index d57308a7..21bc2cdc 100644 --- a/lib/src/runtime/options.dart +++ b/lib/src/runtime/options.dart @@ -1,12 +1,36 @@ -part of '../../runtime.dart'; +import 'package:meta/meta.dart' show internal; +import '../../driver.dart'; + +/// Engine-specific settings applied before a transaction callback starts. +/// +/// The backend type prevents passing one engine's settings to another engine's +/// typed database. Runtime validation also checks the selected dialect. +/// +/// {@category Execution} sealed class TransactionOptions { + /// Creates typed transaction settings without starting a transaction. const TransactionOptions(); + + /// Engine these transaction settings are valid for. SqlDialect get dialect; + + /// @nodoc + @internal List get beginCommands; } -enum Isolation { readCommitted, repeatableRead, serializable } +/// Requested transaction isolation; exact visibility rules belong to the engine. +enum Isolation { + /// Reads committed data according to the engine's per-statement semantics. + readCommitted, + + /// Uses the engine's repeatable-read semantics across the transaction. + repeatableRead, + + /// Requests serializable execution; conflicting work may require a retry. + serializable, +} String _isolationSql(Isolation isolation) => switch (isolation) { Isolation.readCommitted => 'READ COMMITTED', @@ -14,54 +38,98 @@ String _isolationSql(Isolation isolation) => switch (isolation) { Isolation.serializable => 'SERIALIZABLE', }; +/// PostgreSQL isolation and read-only settings for one transaction. final class PostgresTransaction extends TransactionOptions { + /// Requested PostgreSQL isolation; defaults to read committed. final Isolation isolation; + + /// Whether PostgreSQL should reject writes in this transaction. final bool readOnly; + + /// Creates PostgreSQL settings applied by the transaction BEGIN statement. const PostgresTransaction({ this.isolation = Isolation.readCommitted, this.readOnly = false, }); @override SqlDialect get dialect => SqlDialect.postgres; + + /// @nodoc + @internal @override List get beginCommands => [ 'BEGIN ISOLATION LEVEL ${_isolationSql(isolation)} ${readOnly ? 'READ ONLY' : 'READ WRITE'}', ]; } -enum SqliteTransactionMode { deferred, immediate, exclusive } +/// When SQLite acquires its transaction locks. +enum SqliteTransactionMode { + /// Defers lock acquisition until the first read or write. + deferred, + + /// Attempts to reserve write access as the transaction begins. + immediate, + /// Requests exclusive access; blocking behavior depends on the journal mode. + exclusive, +} + +/// SQLite BEGIN mode for an explicit transaction. final class SqliteTransaction extends TransactionOptions { + /// Lock acquisition mode; defaults to SQLite deferred transactions. final SqliteTransactionMode mode; + + /// Chooses when SQLite attempts to acquire transaction locks. const SqliteTransaction({this.mode = SqliteTransactionMode.deferred}); @override SqlDialect get dialect => SqlDialect.sqlite; + + /// @nodoc + @internal @override List get beginCommands => ['BEGIN ${mode.name.toUpperCase()}']; } +/// MySQL isolation and read-only settings applied to the next transaction. final class MysqlTransaction extends TransactionOptions { + /// Requested MySQL isolation; defaults to repeatable read. final Isolation isolation; + + /// Whether MySQL should reject writes in this transaction. final bool readOnly; + + /// Defaults to repeatable read, independently of prior connection settings. const MysqlTransaction({ this.isolation = Isolation.repeatableRead, this.readOnly = false, }); @override SqlDialect get dialect => SqlDialect.mysql; + + /// @nodoc + @internal @override List get beginCommands => _mysqlBegin(isolation, readOnly); } +/// MariaDB isolation and read-only settings applied to the next transaction. final class MariadbTransaction extends TransactionOptions { + /// Requested MariaDB isolation; defaults to repeatable read. final Isolation isolation; + + /// Whether MariaDB should reject writes in this transaction. final bool readOnly; + + /// Defaults to repeatable read, independently of prior connection settings. const MariadbTransaction({ this.isolation = Isolation.repeatableRead, this.readOnly = false, }); @override SqlDialect get dialect => SqlDialect.mariadb; + + /// @nodoc + @internal @override List get beginCommands => _mysqlBegin(isolation, readOnly); } diff --git a/lib/src/runtime/session_connection.dart b/lib/src/runtime/session_connection.dart deleted file mode 100644 index 99aa9ce2..00000000 --- a/lib/src/runtime/session_connection.dart +++ /dev/null @@ -1,96 +0,0 @@ -part of '../../runtime.dart'; - -/// A session's public execution port. The underlying physical lease and its -/// transaction controls stay private to the runtime. -final class _SessionConnection( - final SqlConnection inner, - final SqlDatabase owner, -) implements SqlConnection { - @override - bool? get transactionActive => inner.transactionActive; - - Future _run(Future Function() action, {String? sql}) { - final result = Future.sync(() async { - owner.checkActive(); - try { - if (sql != null && - owner.inTransaction && - (owner.dialect == SqlDialect.mysql || - owner.dialect == SqlDialect.mariadb)) { - _checkMysqlTransactionSql(sql); - } - return await action(); - } catch (_) { - owner.markFailed(); - rethrow; - } - }); - owner._track( - result.then((_) {}, onError: (Object _, StackTrace _) {}), - ); - return result; - } - - @override - Future execute( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }) => _run(() => inner.execute(command, options: options), sql: command.sql); - - @override - Future openCursor( - SqlCommand command, { - ExecutionOptions options = const ExecutionOptions(), - }) => _run(() async { - final cursor = _SessionCursor( - await inner.openCursor(command, options: options), - this, - ); - owner._cursors.add(cursor.close); - return cursor; - }, sql: command.sql); - - @override - Future invalidate() { - owner.checkActive(); - owner.markFailed(); - return inner.invalidate(); - } -} - -final class _SessionCursor( - final SqlCursor inner, - final _SessionConnection connection, -) implements SqlCursor { - Future? _closing; - @override - Future fetch( - int count, { - ExecutionOptions options = const ExecutionOptions(), - }) => connection._run(() { - if (_closing != null) { - throw const OrmException('CURSOR.CLOSED', 'Cursor has ended.'); - } - return inner.fetch(count, options: options); - }); - // Cleanup is allowed after the callback/deadline ends and runs only once. - @override - Future close() => _closing ??= _close(); - Future _close() async { - try { - await inner.close(); - } catch (_) { - connection.owner.markFailed(); - rethrow; - } finally { - connection.owner._cursors.remove(close); - } - } -} - -SqlConnection _physicalConnection(SqlConnection connection) => - switch (connection) { - _SessionConnection() => _physicalConnection(connection.inner), - _TransactionConnection() => _physicalConnection(connection.inner), - _ => connection, - }; diff --git a/lib/src/runtime/stream.dart b/lib/src/runtime/stream.dart deleted file mode 100644 index 36c22f2a..00000000 --- a/lib/src/runtime/stream.dart +++ /dev/null @@ -1,216 +0,0 @@ -part of '../../runtime.dart'; - -extension SqlDatabaseStreaming on SqlDatabase { - /// Stream raw rows from a database cursor with demand-driven batch fetches. - Stream> stream( - SqlCommand command, { - int batchSize = 128, - ExecutionOptions options = const ExecutionOptions(), - }) => streamRows( - command, - batchSize: batchSize, - options: options, - decode: (_, rows, _) async => rows, - ); - - Stream streamRows( - SqlCommand command, { - required int batchSize, - required ExecutionOptions options, - required Future> Function( - SqlConnection, - List>, - ExecutionOptions, - ) - decode, - }) { - if (batchSize < 1) throw ArgumentError.value(batchSize, 'batchSize'); - if (!capabilities.streaming) { - throw const OrmException( - 'CAPABILITY.STREAM', - 'This driver does not support database cursors.', - ); - } - if ((options.cancellation != null || options.timeout != null) && - !capabilities.cancellation) { - throw const OrmException( - 'CAPABILITY.CANCEL', - 'This driver cannot cancel a running statement.', - ); - } - options.check(); - if (command.parameters.length > capabilities.maxParameters) { - throw const OrmException( - 'QUERY.PARAMETERS', - 'Query exceeds the driver parameter limit.', - ); - } - final cancellation = CancellationToken(); - final execution = ExecutionOptions( - cancellation: capabilities.cancellation ? cancellation : null, - timeout: options.timeout, - ); - final finished = Completer(); - Completer? demand; - var stopped = false, started = false; - Object? cleanupFailure, requestedError; - late StreamController controller; - void requestStop() { - stopped = true; - cancellation.cancel(); - demand?.complete(); - demand = null; - } - - Future stop() async { - requestStop(); - if (started) await finished.future; - if (cleanupFailure != null) throw cleanupFailure!; - } - - Future waitForDemand() async { - if (controller.isPaused && !stopped) { - demand ??= Completer(); - await demand!.future; - } - } - - Future observe( - QueryOperation operation, - Future Function() action, - ) async { - final watch = onQuery == null ? null : (Stopwatch()..start()); - Object? error; - T? result; - try { - return result = await action(); - } catch (e) { - error = e; - if (inTransaction) _statementFailed = true; - rethrow; - } finally { - watch?.stop(); - try { - onQuery?.call( - QueryEvent( - operation: operation, - sql: command.sql, - parameterCount: operation == .cursorOpen - ? command.parameters.length - : 0, - elapsed: watch!.elapsed, - rowCount: result is SqlResult ? result.rows.length : null, - error: error, - ), - ); - } catch (_) {} - } - } - - Future produce() async { - started = true; - _streams.add(stop); - final unsubscribe = options.cancellation?.listen(() { - requestedError = const OrmException( - 'OPERATION.CANCELLED', - 'Stream cancelled.', - ); - requestStop(); - }); - try { - await run( - (connection) async { - final ownsTransaction = !inTransaction; - SqlCursor? cursor; - var complete = false, began = false; - try { - if (stopped) return; - if (ownsTransaction) { - await _executeOn( - connection is _SessionConnection - ? connection.inner - : connection, - SqlCommand(switch (dialect) { - SqlDialect.postgres => 'BEGIN READ ONLY', - SqlDialect.mysql || - SqlDialect.mariadb => 'START TRANSACTION READ ONLY', - SqlDialect.sqlite => 'BEGIN', - }), - ); - began = true; - } - cursor = await observe( - .cursorOpen, - () => connection.openCursor(command, options: execution), - ); - while (!stopped) { - await waitForDemand(); - if (stopped) break; - final batch = await observe( - .cursorFetch, - () => cursor!.fetch(batchSize, options: execution), - ); - if (stopped) break; - final rows = await decode(connection, batch.rows, execution); - for (final row in rows) { - await waitForDemand(); - if (stopped) break; - controller.add(row); - } - if (batch.rows.length < batchSize) { - complete = !stopped; - break; - } - } - } finally { - try { - if (cursor != null) await observe(.cursorClose, cursor.close); - if (began) { - await _executeOn( - connection is _SessionConnection - ? connection.inner - : connection, - SqlCommand(complete ? 'COMMIT' : 'ROLLBACK'), - ); - } - } catch (e) { - cleanupFailure = e; - if (inTransaction) _active = false; - await connection.invalidate(); - rethrow; - } - } - }, - acquire: AcquisitionOptions( - timeout: options.acquireTimeout, - cancellation: cancellation, - ), - ); - } catch (error, stack) { - if (!stopped) controller.addError(error, stack); - } finally { - unsubscribe?.call(); - if (requestedError != null) controller.addError(requestedError!); - _streams.remove(stop); - finished.complete(); - // A cancellation cleanup error is already exposed by stop() or the - // error event. Do not report it again through an unobserved close future. - unawaited(controller.close().catchError((Object _) {})); - } - } - - controller = StreamController( - sync: true, - onListen: () => unawaited(produce()), - onPause: () { - demand ??= Completer(); - }, - onResume: () { - demand?.complete(); - demand = null; - }, - onCancel: stop, - ); - return controller.stream; - } -} diff --git a/lib/src/transaction.dart b/lib/src/runtime/transaction.dart similarity index 82% rename from lib/src/transaction.dart rename to lib/src/runtime/transaction.dart index ebb13c9d..70770d19 100644 --- a/lib/src/transaction.dart +++ b/lib/src/runtime/transaction.dart @@ -1,7 +1,14 @@ -part of '../runtime.dart'; +import 'dart:async'; +import 'dart:math' as math; + +import 'package:meta/meta.dart' show internal; + +import '../../driver.dart'; /// Explicit opt-in: callback logic must be safe to repeat after rollback. /// The budget counts every scheduled retry, including commit-only retries. +/// +/// {@category Execution} final class TransactionRetry { /// Initial attempt plus callback replays and COMMIT-only retries combined. final int maxAttempts; @@ -14,13 +21,18 @@ final class TransactionRetry { /// Upper bound as the backoff doubles after each retry. final Duration maxDelay; + + /// Enables bounded retries only for callbacks safe to repeat after rollback. const TransactionRetry({ this.maxAttempts = 3, this.timeout = const Duration(seconds: 30), this.delay = const Duration(milliseconds: 10), this.maxDelay = const Duration(milliseconds: 250), }); - void _check() { + + /// @nodoc + @internal + void validate() { if (maxAttempts < 1 || timeout <= Duration.zero || delay.isNegative || @@ -34,16 +46,20 @@ final class TransactionRetry { // Created only after the attempt has confirmed rollback; never for acquisition, // uncertain COMMIT, failed cleanup or an application-caught statement failure. -final class _RetryAfterRollback(final SqlFailure error, final StackTrace stack) +/// @nodoc +@internal +final class RetryAfterRollback(final SqlFailure error, final StackTrace stack) implements Exception {} -final class _RetryBudget { +/// @nodoc +@internal +final class RetryBudget { final TransactionRetry policy; final Stopwatch _clock = Stopwatch()..start(); final math.Random _random = math.Random(); int _attempts = 1; int _delay; - _RetryBudget(this.policy) : _delay = policy.delay.inMicroseconds; + RetryBudget(this.policy) : _delay = policy.delay.inMicroseconds; Duration get remaining => policy.timeout - _clock.elapsed; Duration limit(Duration? other) { final left = remaining; @@ -56,7 +72,7 @@ final class _RetryBudget { return other == null || left < other ? left : other; } - Future next(_TransactionControl control) async { + Future next(TransactionControl control) async { control.check(); limit(null); if (_attempts >= policy.maxAttempts) return false; @@ -72,10 +88,12 @@ final class _RetryBudget { } } -final class _CancellationLink { +/// @nodoc +@internal +final class CancellationLink { final token = CancellationToken(); final List _remove = []; - _CancellationLink(Iterable sources) { + CancellationLink(Iterable sources) { for (final source in sources) { _remove.add(source.listen(token.cancel)); } @@ -87,7 +105,9 @@ final class _CancellationLink { } } -final class _TransactionControl { +/// @nodoc +@internal +final class TransactionControl { final CancellationToken token = CancellationToken(); final Duration? timeout; final Stopwatch _clock = Stopwatch()..start(); @@ -96,7 +116,7 @@ final class _TransactionControl { void Function()? _unsubscribe; OrmException? failure; - _TransactionControl(this.timeout, CancellationToken? cancellation) { + TransactionControl(this.timeout, CancellationToken? cancellation) { _expired.future.ignore(); _unsubscribe = cancellation?.listen(() => _expire(false)); if (timeout != null) _timer = Timer(timeout!, () => _expire(true)); @@ -130,7 +150,7 @@ final class _TransactionControl { final link = options.cancellation == null || identical(options.cancellation, token) ? null - : _CancellationLink([token, options.cancellation!]); + : CancellationLink([token, options.cancellation!]); try { return await action( ExecutionOptions( @@ -175,9 +195,11 @@ final class _TransactionControl { } } -final class _TransactionConnection( +/// @nodoc +@internal +final class TransactionConnection( final SqlConnection inner, - final _TransactionControl control, + final TransactionControl control, ) implements SqlConnection { @override bool? get transactionActive => inner.transactionActive; @@ -190,7 +212,7 @@ final class _TransactionConnection( Future openCursor( SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), - }) async => _TransactionCursor( + }) async => TransactionCursor( await control.execute( (o) => inner.openCursor(command, options: o), options, @@ -201,9 +223,11 @@ final class _TransactionConnection( Future invalidate() => inner.invalidate(); } -final class _TransactionCursor( +/// @nodoc +@internal +final class TransactionCursor( final SqlCursor inner, - final _TransactionControl control, + final TransactionControl control, ) implements SqlCursor { @override Future fetch( @@ -215,5 +239,5 @@ final class _TransactionCursor( Future close() => inner.close(); } -SqlConnection _unscoped(SqlConnection connection) => - connection is _TransactionConnection ? connection.inner : connection; +SqlConnection unscopedConnection(SqlConnection connection) => + connection is TransactionConnection ? connection.inner : connection; diff --git a/lib/src/schema/declaration.dart b/lib/src/schema/declaration.dart new file mode 100644 index 00000000..55c7a593 --- /dev/null +++ b/lib/src/schema/declaration.dart @@ -0,0 +1,304 @@ +import '../../values.dart' show Codec; +import 'model.dart' show ComputedStorage; + +/// A public const codec reference. Generation reads its type and storage tag; +/// it never executes the application's encode/decode functions. +final class UseCodec { + /// Public constant codec used to encode and decode the annotated field. + final Codec codec; + + /// Associates a field with a public const variable or static const codec. + const UseCodec(this.codec); +} + +/// Stable database text for an enum constant, independent of its Dart name. +final class EnumValue { + /// Unique text label stored for the annotated enum constant. + final String value; + + /// Overrides the default storage label, which is the Dart constant name. + const EnumValue(this.value); +} + +/// Marks a field as part of the table's primary key. +/// +/// Multiple [Id] fields form a composite key in field order. Use [Id.generated] +/// only for a single, non-null integer primary key. +/// +/// ```dart +/// final class User(@Id.generated() final int id, final String name); +/// ``` +/// +/// {@category Declarations} +final class Id { + /// Whether the database generates the identity when an insert omits it. + final bool generated; + + /// Declares an application-supplied primary-key field. + const Id() : generated = false; + + /// Declares a database-generated integer primary key. + const Id.generated() : generated = true; +} + +/// Declares a unique constraint for the annotated field. +final class Unique { + /// Requires stored non-null values to be unique under database semantics. + const Unique(); +} + +/// Overrides the physical column name while preserving the Dart field name. +final class ColumnName { + /// Physical SQL column name, quoted as an identifier during SQL generation. + final String name; + + /// Associates a Dart field with a stable database column name. + const ColumnName(this.name); +} + +/// A database SQL default used when an insert omits the annotated field. +/// +/// The expression is trusted SQL, not a bound parameter. It does not supply a +/// Dart constructor default or run when an existing row is decoded. +final class Default { + /// SQL expression placed in the column DEFAULT clause. + final String expression; + + /// Declares a database default such as `CURRENT_TIMESTAMP` or `false`. + const Default.sql(this.expression); +} + +/// Generates an omitted insert value in Dart. This never declares SQL DEFAULT. +/// The factory is referenced during generation, not invoked. +final class ClientDefault { + /// Public, synchronous factory invoked only when constructing an insert. + final T Function() factory; + + /// Uses a public top-level function, static method, or constructor tear-off. + const ClientDefault(this.factory); +} + +/// SQL computed by the database; omitted from generated writes. +final class Computed { + /// Default trusted SQL expression used when a dialect override is absent. + final String expression; + + /// Optional SQLite expression using physical column names. + final String? sqlite; + + /// Optional PostgreSQL expression using physical column names. + final String? postgres; + + /// Optional MySQL expression using physical column names. + final String? mysql; + + /// Optional MariaDB expression using physical column names. + final String? mariadb; + + /// Whether the database stores the result or computes it when read. + final ComputedStorage storage; + + /// Declares a generated column excluded from generated create and patch APIs. + const Computed.sql( + this.expression, { + this.sqlite, + this.postgres, + this.mysql, + this.mariadb, + this.storage = ComputedStorage.stored, + }); +} + +/// Signed integer column storage. SQL expression results retain the int codec. +final class IntegerBits { + /// Signed storage width: 16, 32, or 64 bits. + final int value; + + /// Constrains an integer column; unsupported widths fail generation. + const IntegerBits(this.value); +} + +/// Decimal column precision and scale, matching PostgreSQL NUMERIC(p, s). +final class DecimalDigits { + /// Total declared decimal precision, from 1 through 1000. + final int precision; + + /// Declared scale, from -1000 through 1000; defaults to zero. + final int scale; + + /// Constrains decimal column storage; each database validates its own limits. + const DecimalDigits(this.precision, [this.scale = 0]); +} + +/// Fractional second digits (0..6) for time, local timestamp or UTC instant columns. +final class TemporalPrecision { + /// Number of fractional second digits, from 0 through 6. + final int digits; + + /// Declares storage precision without changing the Dart field type. + const TemporalPrecision(this.digits); +} + +/// Database behavior when a referenced row is deleted. +enum ReferentialAction { + /// Rejects deletion while referencing rows exist. + restrict, + + /// Deletes rows that reference the deleted row. + cascade, + + /// Sets referencing columns to SQL NULL; those columns must be nullable. + setNull, + + /// Assigns the referencing columns' database defaults when supported. + setDefault, + + /// Uses the database's NO ACTION constraint semantics. + noAction, +} + +/// A fixed SQL file with declared result and parameter Record types. +/// Run query generation, then check the output against each target database. +/// Paths are relative to this declaration's Dart library. [R] is a named-record +/// result typedef; [P] is a named record or `()` for no parameters. +/// +/// ```dart +/// typedef UserName = ({String name}); +/// final userNames = sqlQuery( +/// sqlite: 'user_names.sql', +/// ); +/// ``` +/// +/// {@category Declarations} +SqlDeclaration sqlQuery({ + String? sqlite, + String? postgres, + String? mysql, + String? mariadb, +}) => SqlDeclaration(sqlite, postgres, mysql, mariadb); + +/// Source metadata for a typed SQL query; creating it does not execute SQL. +/// +/// Use [sqlQuery] in a top-level declaration recognized by the generator. +final class SqlDeclaration { + /// SQLite SQL file, relative to the declaring Dart library. + final String? sqlite; + + /// PostgreSQL SQL file, relative to the declaring Dart library. + final String? postgres; + + /// MySQL SQL file, relative to the declaring Dart library. + final String? mysql; + + /// MariaDB SQL file, relative to the declaring Dart library. + final String? mariadb; + + /// Stores dialect-specific source paths without opening them. + const SqlDeclaration(this.sqlite, this.postgres, this.mysql, this.mariadb); +} + +/// Declares an independently named table backed by the Dart model [M]. +/// +/// Place this call in a public top-level variable. [M] must be a supported final +/// primary-constructor class or named-record typedef in the same source file. +/// The variable name determines the default physical name unless [table] is set. +/// Model constructors are not executed during generation. +/// +/// ```dart +/// final class User(@Id() final int id, final String email); +/// final users = entity(table: 'app_users'); +/// final emailIndex = users.index((user) => user.email); +/// ``` +/// +/// {@category Declarations} +Entity entity({String? table}) => Entity(table); + +/// A source-level table declaration with model, key, and constraint types. +/// +/// Use [entity] to create a declaration the generator can discover. Constraint +/// methods describe source syntax; calling them does not modify a live database. +/// +/// {@category Declarations} +final class Entity { + /// Explicit physical table name, or null to derive it from the declaration. + final String? table; + + /// Stores the optional physical name without registering or opening a table. + const Entity(this.table); + + /// Selects one field or a record of fields for a relationship key. + /// + /// Selector order determines composite-key column order. + EntityKey key(K Function(M) selector) => EntityKey(this, selector); + + /// Declares a primary key in a separate top-level variable. + /// + /// Selected columns must be non-nullable and cannot duplicate an annotated key. + SchemaConstraint primaryKey(K Function(M) selector) => + const SchemaConstraint(); + + /// Declares a unique key on one field or an ordered record of fields. + SchemaConstraint unique(K Function(M) selector) => + const SchemaConstraint(); + + /// Trusted SQL using physical column names. Omitted name uses the declaration + /// name; explicit name: null preserves an unnamed imported constraint. + SchemaConstraint check( + String expression, { + String? name, + String? sqlite, + String? postgres, + String? mysql, + String? mariadb, + }) => const SchemaConstraint(); + + /// Declares an index on the selected physical columns in selector order. + /// + /// The top-level declaration supplies the default name; [name] overrides it. + /// [unique] also permits this index to serve as a foreign-key target. + SchemaConstraint index( + K Function(M) selector, { + String? name, + bool unique = false, + }) => const SchemaConstraint(); +} + +/// An ordered set of model fields used to declare a relationship. +/// +/// Obtain this through [Entity.key]. The generator reads the selector syntax and +/// does not call it against an application object. +final class EntityKey { + /// Table declaration owning the selected fields. + final Entity entity; + + /// Source selector for one field or a record of fields. + final K Function(M) selector; + + /// Stores a table and selector without evaluating the selector. + const EntityKey(this.entity, this.selector); + + /// Declares a foreign key and generated query navigation to [target]. + /// + /// The target must be a primary or unique key in matching column order, with + /// compatible Dart and storage types. [inverse] adds reverse navigation; + /// [onDelete] controls database-enforced deletion behavior. + SchemaConstraint references( + EntityKey target, { + String? inverse, + ReferentialAction onDelete = ReferentialAction.restrict, + }) => const SchemaConstraint(); + + /// Read-only query navigation without a database foreign key. Matching rows + /// may be absent or duplicated; this declaration creates no write effects. + SchemaConstraint relatesTo(EntityKey target, {String? inverse}) => + const SchemaConstraint(); +} + +/// Marker returned by source-level key, index, check, and relation declarations. +/// +/// The generator reads the original top-level expression. This marker carries no +/// runtime database operation or mutable constraint registry. +final class SchemaConstraint { + /// Creates a marker without evaluating or applying a schema change. + const SchemaConstraint(); +} diff --git a/lib/src/schema/model.dart b/lib/src/schema/model.dart new file mode 100644 index 00000000..1ac74c33 --- /dev/null +++ b/lib/src/schema/model.dart @@ -0,0 +1,294 @@ +import '../../driver.dart' show SqlDialect; +import '../../values.dart'; + +/// Physical column metadata coupled to the Dart/storage [Codec]. +/// +/// This value is usable without generated models or a connection. Schema and +/// migration validation enforce combinations such as generated identities, +/// nullability, defaults, and engine-specific storage limits. +/// +/// {@category Schema} +final class Column { + /// Physical SQL name, quoted as an identifier when SQL is rendered. + final String name; + + /// Typed conversion used for stored values and selected results. + final Codec codec; + + /// Whether the physical column permits SQL NULL. + final bool nullable; + + /// Whether the database supplies an integer primary-key identity. + final bool generated; + + /// Trusted SQL default expression used when an insert omits this column. + final String? defaultSql; + + /// Database-generated expression; computed columns are excluded from writes. + final ComputedColumn? computed; + + /// Called once for an omitted value when constructing an insert. Prepared + /// mutations retain that value; compilation and updates never call this. + final T Function()? clientDefault; + + /// Signed integer storage width (16, 32 or 64). The default is 64. + /// This describes the column, not the result width of SQL arithmetic. + final int? integerBits; + + /// Optional decimal column precision, separate from expression result types. + final int? decimalPrecision; + + /// Optional decimal column scale, paired with [decimalPrecision]. + final int? decimalScale; + + /// Optional fractional second precision, from 0 through 6. + final int? temporalPrecision; + + /// Describes one physical column without creating or validating a database. + const Column( + this.name, + this.codec, { + this.nullable = false, + this.generated = false, + this.defaultSql, + this.computed, + this.clientDefault, + this.integerBits, + this.decimalPrecision, + this.decimalScale, + this.temporalPrecision, + }); +} + +/// How a database maintains a computed column, subject to engine capabilities. +enum ComputedStorage { + /// Stores the generated value and recomputes it when its inputs change. + stored, + + /// Computes the generated value when it is read. + virtual, +} + +/// Database-computed SQL using physical column names. +final class ComputedColumn { + /// Trusted SQLite expression using physical column names. + final String sqlite; + + /// Trusted PostgreSQL expression using physical column names. + final String postgres; + + /// Explicit MySQL expression, or null when that dialect is unsupported. + final String? mysql; + + /// Explicit MariaDB expression, or null when that dialect is unsupported. + final String? mariadb; + + /// Storage strategy requested from the target database. + final ComputedStorage storage; + + /// Uses one trusted SQL expression for every supported dialect. + const ComputedColumn( + String expression, { + this.storage = ComputedStorage.stored, + }) : sqlite = expression, + postgres = expression, + mysql = expression, + mariadb = expression; + + /// Uses explicit dialect expressions without silently substituting another. + const ComputedColumn.forDialects({ + required this.sqlite, + required this.postgres, + this.mysql, + this.mariadb, + this.storage = ComputedStorage.stored, + }); + + /// Returns the target expression or throws if that dialect was not declared. + String expression(SqlDialect dialect) => switch (dialect) { + SqlDialect.sqlite => sqlite, + SqlDialect.postgres => postgres, + SqlDialect.mysql => + mysql ?? + (throw const OrmException( + 'SCHEMA.DIALECT', + 'Declare the MySQL expression explicitly.', + )), + SqlDialect.mariadb => + mariadb ?? + (throw const OrmException( + 'SCHEMA.DIALECT', + 'Declare the MariaDB expression explicitly.', + )), + }; +} + +/// A database foreign key described with ordered physical column names. +/// +/// Source and target columns must have matching arity and compatible storage. +/// Creating this value does not execute DDL or infer an index. +final class ForeignKey { + /// Source columns in key order. + final List columns; + + /// Physical name of the referenced table. + final String target; + + /// Target key columns in the corresponding source-column order. + final List targetColumns; + + /// SQL referential action, validated for the target database. + final String onDelete; + + /// Describes a foreign key; the default deletion action is `RESTRICT`. + const ForeignKey( + this.columns, + this.target, + this.targetColumns, { + this.onDelete = 'RESTRICT', + }); +} + +/// A row CHECK expression. A null name leaves naming to the database. +final class CheckSchema { + /// Physical constraint name, or null to retain an unnamed constraint. + final String? name; + + /// Trusted SQLite expression using physical column names. + final String sqlite; + + /// Trusted PostgreSQL expression using physical column names. + final String postgres; + + /// Explicit MySQL expression, or null when that dialect is unsupported. + final String? mysql; + + /// Explicit MariaDB expression, or null when that dialect is unsupported. + final String? mariadb; + + /// Uses one trusted SQL expression for all dialects. + const CheckSchema(this.name, String expression) + : sqlite = expression, + postgres = expression, + mysql = expression, + mariadb = expression; + + /// Uses explicit dialect expressions; omitted engines remain unsupported. + const CheckSchema.forDialects( + this.name, { + required this.sqlite, + required this.postgres, + this.mysql, + this.mariadb, + }); + + /// Returns the target expression or rejects an undeclared dialect. + String expression(SqlDialect dialect) => switch (dialect) { + SqlDialect.sqlite => sqlite, + SqlDialect.postgres => postgres, + SqlDialect.mysql => + mysql ?? + (throw const OrmException( + 'SCHEMA.DIALECT', + 'Declare the MySQL expression explicitly.', + )), + SqlDialect.mariadb => + mariadb ?? + (throw const OrmException( + 'SCHEMA.DIALECT', + 'Declare the MariaDB expression explicitly.', + )), + }; +} + +/// A simple index over ordered physical columns. +final class IndexSchema { + /// Physical SQL index name. + final String name; + + /// Indexed columns in the requested index order. + final List columns; + + /// Whether the database enforces uniqueness for this index. + final bool unique; + + /// Describes an index without creating it or inferring sort expressions. + const IndexSchema(this.name, this.columns, {this.unique = false}); +} + +/// Immutable physical table metadata, independent of Dart model identity. +/// +/// The constructor copies collection inputs, including nested key and index +/// column lists. Schema consumers validate engine support before executing DDL. +/// +/// ```dart +/// final accounts = TableSchema( +/// 'accounts', +/// columns: [Column('id', Codecs.integer), Column('email', Codecs.text)], +/// primaryKey: ['id'], +/// uniqueKeys: [['email']], +/// ); +/// ``` +/// +/// {@category Schema} +final class TableSchema { + /// Physical table name. + final String name; + + /// Ordered columns used to render the physical schema. + final List> columns; + + /// Ordered primary-key column names, or an empty list when absent. + final List primaryKey; + + /// Ordered column groups with database uniqueness constraints. + final List> uniqueKeys; + + /// Database-enforced references to keys in other physical tables. + final List foreignKeys; + + /// Explicit indexes; foreign keys do not imply extra indexes here. + final List indexes; + + /// Database CHECK constraints expressed in trusted SQL. + final List checks; + + /// Columns with Dart insert factories, derived from [columns]. + final List> clientDefaults; + + /// Copies schema collections without opening or altering a database. + TableSchema( + this.name, { + required List> columns, + List primaryKey = const [], + List> uniqueKeys = const [], + List foreignKeys = const [], + List indexes = const [], + List checks = const [], + }) : columns = List.unmodifiable(columns), + clientDefaults = List.unmodifiable( + columns.where((c) => c.clientDefault != null), + ), + primaryKey = List.unmodifiable(primaryKey), + uniqueKeys = List.unmodifiable( + uniqueKeys.map(List.unmodifiable), + ), + foreignKeys = List.unmodifiable([ + for (final key in foreignKeys) + ForeignKey( + List.unmodifiable(key.columns), + key.target, + List.unmodifiable(key.targetColumns), + onDelete: key.onDelete, + ), + ]), + checks = List.unmodifiable(checks), + indexes = List.unmodifiable([ + for (final index in indexes) + IndexSchema( + index.name, + List.unmodifiable(index.columns), + unique: index.unique, + ), + ]); +} diff --git a/lib/src/sql.dart b/lib/src/sql.dart deleted file mode 100644 index 4e4f17f2..00000000 --- a/lib/src/sql.dart +++ /dev/null @@ -1,435 +0,0 @@ -part of '../sql.dart'; - -sealed class _Node { - const _Node(); - String write(_Writer w) => w.project?.call(this) ?? writeSql(w); - String writeSql(_Writer w); -} - -_Node _unwrapStorage(_Node node) => switch (node) { - _DecimalNode(:final child) || _TemporalNode(:final child) => child, - _ => node, -}; - -final class _ColumnNode(final TableRef table, final String name) extends _Node { - @override - String writeSql(_Writer w) { - final alias = w.aliases[table]; - if (alias == null) { - throw const OrmException( - 'QUERY.SCOPE', - 'Column belongs to another query.', - ); - } - if (w.mysql && alias == 'excluded') return 'VALUES(${w.quote(name)})'; - return w.unqualified || w.unqualifiedTable == table - ? w.quote(name) - : '${w.quote(alias)}.${w.quote(name)}'; - } -} - -final class _Parameter( - final Object? value, { - final String? sqlType, - final String? storageType, -}) extends _Node { - @override - String writeSql(_Writer w) { - final parameter = w.parameter(value, storageType: storageType ?? sqlType); - if (w.mysql && (storageType ?? sqlType) == 'json') { - return w.dialect == SqlDialect.mysql - ? 'CAST($parameter AS JSON)' - : "JSON_EXTRACT($parameter, '\$')"; - } - if (w.mysql && - {'decimal', 'bigint'}.contains(storageType ?? sqlType) && - value != null) { - final decimal = Decimal.parse(value as String); - final text = decimal.toString(); - final digits = text.replaceAll('-', '').split('.'); - final scale = digits.length == 2 ? digits[1].length : 0; - final precision = digits[0].length + scale; - if (precision > 65 || scale > 30) { - throw const OrmException( - 'CAPABILITY.DECIMAL', - 'MySQL/MariaDB SQL decimals require at most 65 digits and scale 30.', - ); - } - return 'CAST($parameter AS DECIMAL(65,$scale))'; - } - // A standalone SELECT parameter has no column context in PostgreSQL and - // otherwise resolves to text, including inside a UNION operand. - if (sqlType == null || w.dialect != SqlDialect.postgres) return parameter; - final type = switch (sqlType) { - 'integer' => 'BIGINT', - 'bigint' || 'decimal' => 'NUMERIC', - 'text' => 'TEXT', - 'real' => 'DOUBLE PRECISION', - 'boolean' => 'BOOLEAN', - 'timestamp' || 'instant' => 'TIMESTAMPTZ', - 'date' => 'DATE', - 'time' => 'TIME WITHOUT TIME ZONE', - 'local_datetime' => 'TIMESTAMP WITHOUT TIME ZONE', - 'json' => 'JSONB', - 'blob' => 'BYTEA', - _ => throw OrmException( - 'QUERY.PARAMETER_TYPE', - 'No PostgreSQL parameter type for $sqlType.', - ), - }; - return 'CAST($parameter AS $type)'; - } -} - -final class _Binary(final _Node left, final String op, final _Node right) - extends _Node { - @override - String writeSql(_Writer w) => '(${left.write(w)} $op ${right.write(w)})'; -} - -final class _Unary( - final String op, - final _Node child, { - final bool postfix = false, -}) extends _Node { - @override - String writeSql(_Writer w) => - postfix ? '(${child.write(w)} $op)' : '($op ${child.write(w)})'; -} - -final class _Function( - final String name, - final List<_Node> arguments, { - final bool distinct = false, - final bool decimal = false, -}) extends _Node { - @override - String writeSql(_Writer w) { - if (decimal && w.mysql && name == 'SUM') { - throw const OrmException( - 'CAPABILITY.DECIMAL_PRECISION', - 'MySQL/MariaDB decimal sums can silently saturate in subqueries; use explicit native SQL when its precision is acceptable.', - ); - } - return '${decimal && w.dialect == SqlDialect.sqlite ? 'orm_decimal_${name.toLowerCase()}_v1' : name}(${distinct ? 'DISTINCT ' : ''}' - '${arguments.map((e) => e.write(w)).join(', ')})'; - } -} - -final class _In(final _Node expression, final List<_Node> values) - extends _Node { - @override - String writeSql(_Writer w) => values.isEmpty - ? 'FALSE' - : '(${expression.write(w)} IN (${values.map((v) => v.write(w)).join(', ')}))'; -} - -/// Trusted SQL fragments still bind embedded expressions as parameters or ASTs. -final class _Raw(final List parts, final List<_Node> values) - extends _Node { - @override - String writeSql(_Writer w) { - if (w.reads?.includeRaw == true) w.reads!.opaque = true; - final result = StringBuffer(parts.first); - for (var i = 0; i < values.length; i++) { - result - ..write(values[i].write(w)) - ..write(parts[i + 1]); - } - return result.toString(); - } -} - -final class _Writer { - final SqlDialect dialect; - final QueryContext? database; - final Map aliases; - final List parameters = []; - final Set leftJoins = {}; - final _ReadTables? reads; - final bool exactDecimal; - final bool temporal; - bool unqualified = false; - TableRef? unqualifiedTable; - String? Function(_Node)? project; - _AverageInputs? averageInputs; - _Writer( - this.dialect, - this.aliases, { - this.database, - this.reads, - this.exactDecimal = false, - this.temporal = false, - }); - bool get mysql => - dialect == SqlDialect.mysql || dialect == SqlDialect.mariadb; - String quote(String name) { - _checkSqlText(name); - return mysql - ? '`${name.replaceAll('`', '``')}`' - : '"${name.replaceAll('"', '""')}"'; - } - - String parameter(Object? value, {String? storageType}) { - // Codec storage, not string pattern guessing, determines temporal binding. - if (mysql && value is String && storageType == 'instant') { - final instant = Codecs.dateTime.decode(value).toUtc(); - if (instant.year < 1000 || instant.year > 9999) { - throw const OrmException( - 'CAPABILITY.TEMPORAL', - 'MySQL timestamps require years 1000 through 9999.', - ); - } - value = value.substring(0, value.length - 3); - } - parameters.add(switch ((dialect, value)) { - (SqlDialect.sqlite, bool v) => v ? 1 : 0, - (SqlDialect.sqlite, DateTime v) => v.toUtc().toIso8601String(), - _ => value, - }); - return dialect == SqlDialect.postgres - ? '\$${parameters.length}' - : mysql - ? '\u0001${parameters.length}\u0002' - : '?${parameters.length}'; - } - - SqlCommand finish(String sql) { - SqlCommand checked(String text, List values) { - if (database != null && - values.length > database!.capabilities.maxParameters) { - throw const OrmException( - 'QUERY.PARAMETERS', - 'SQL exceeds the parameter limit.', - ); - } - return SqlCommand(text, values); - } - - if (!mysql) return checked(sql, parameters); - // Construction may render ORDER BY before WHERE, and window staging may - // reuse a fragment. Positional protocols bind in final SQL occurrence order. - final ordered = []; - final text = sql.replaceAllMapped(RegExp('\u0001([0-9]+)\u0002'), (match) { - ordered.add(parameters[int.parse(match[1]!) - 1]); - return '?'; - }); - return checked(text, ordered); - } -} - -class Expr extends Selection { - final _Node _node; - final Codec codec; - Expr._(_Node node, this.codec) - : _node = switch (codec.sqlType) { - 'decimal' when node is! _DecimalNode => _DecimalNode(node), - 'date' || 'time' || 'local_datetime' || 'instant' - when node is! _TemporalNode => - _TemporalNode(node, codec.sqlType), - _ => node, - }; - - Expr eq(T value) => value == null - ? Expr._( - _Unary('IS NULL', _node, postfix: true), - Codecs.boolean.nullable(), - ) - : _compare( - '=', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr ne(T value) => value == null - ? Expr._( - _Unary('IS NOT NULL', _node, postfix: true), - Codecs.boolean.nullable(), - ) - : _compare( - '<>', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr equals(Expr other) => _compare('=', other._node); - Expr gt(T value) => _compare( - '>', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr gte(T value) => _compare( - '>=', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr lt(T value) => _compare( - '<', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr lte(T value) => _compare( - '<=', - _Parameter(codec.encode(value), storageType: codec.sqlType), - ); - Expr _compare(String op, _Node right) => - Expr._(_Binary(_node, op, right), Codecs.boolean.nullable()); - Expr isNull() => - Expr._(_Unary('IS NULL', _node, postfix: true), Codecs.boolean); - Expr isNotNull() => - Expr._(_Unary('IS NOT NULL', _node, postfix: true), Codecs.boolean); - Expr isIn(Iterable values) => Expr._( - _In(_node, [ - for (final value in values) - _Parameter(codec.encode(value), storageType: codec.sqlType), - ]), - Codecs.boolean.nullable(), - ); - Expr isInQuery(Query query) { - if (query._selection is! Expr) { - throw const OrmException( - 'QUERY.SCALAR', - 'IN subqueries select one SQL expression.', - ); - } - return Expr._( - _Binary(_node, 'IN', _Subquery(query)), - Codecs.boolean.nullable(), - ); - } - - Expr over({ - List> partitionBy = const [], - List orderBy = const [], - WindowFrame? frame, - }) { - final function = _unwrapStorage(_node); - if ((function is! _Function && function is! _DecimalAverage) || - !_aggregate(function)) { - throw const OrmException( - 'QUERY.WINDOW', - 'over() applies to an aggregate expression.', - ); - } - if ([ - ...partitionBy.map((e) => e._node), - ...orderBy.map((o) => o.expression._node), - ].any(_window)) { - throw const OrmException( - 'QUERY.WINDOW', - 'Window partition/order expressions cannot contain another window.', - ); - } - return Expr._( - _WindowNode( - function, - List.unmodifiable(partitionBy), - List.unmodifiable(orderBy), - frame, - ), - codec, - ); - } - - OrderTerm asc({NullOrder? nulls}) => OrderTerm._(this, false, nulls); - OrderTerm desc({NullOrder? nulls}) => OrderTerm._(this, true, nulls); - CursorTerm cursor(T value, {bool descending = false, NullOrder? nulls}) => - CursorTerm._(OrderTerm._(this, descending, nulls), codec.encode(value)); - Expr count({bool distinct = false}) => - Expr._(_Function('COUNT', [_node], distinct: distinct), Codecs.integer); - Expr min() => Expr._(_Function('MIN', [_node]), codec.nullable()); - Expr max() => Expr._(_Function('MAX', [_node]), codec.nullable()); - @override - _Decoder _bind(_SelectionPlan plan) { - plan.require(this); - final index = plan.column(this); - return (row) => codec.decode(row[index]); - } -} - -Expr value(T value, Codec codec) => - Expr._(_Parameter(codec.encode(value), sqlType: codec.sqlType), codec); - -/// [parts] are trusted SQL, [values] are expressions. Never put user input in parts. -Expr sql(List parts, List> values, Codec codec) { - if (parts.length != values.length + 1) { - throw ArgumentError('SQL needs one more text part than expression.'); - } - for (final part in parts) { - _checkSqlText(part); - } - return Expr._(_Raw(List.of(parts), [for (final v in values) v._node]), codec); -} - -void _checkSqlText(String text) { - if (text.contains('\u0001') || text.contains('\u0002')) { - throw const OrmException( - 'SQL.TEXT', - 'SQL text contains a reserved control character.', - ); - } -} - -extension Predicate on Expr { - Expr and(Expr other) => - Expr._(_Binary(_node, 'AND', other._node), Codecs.boolean.nullable()); - Expr or(Expr other) => - Expr._(_Binary(_node, 'OR', other._node), Codecs.boolean.nullable()); - Expr not() => Expr._(_Unary('NOT', _node), Codecs.boolean.nullable()); -} - -extension TextExpression on Expr { - Expr like(String pattern) => _compare('LIKE', _Parameter(pattern)); - Expr lower() => Expr._(_Function('LOWER', [_node]), Codecs.text); - Expr upper() => Expr._(_Function('UPPER', [_node]), Codecs.text); -} - -extension NumericExpression on Expr { - Expr plus(T n) => Expr._( - _Binary( - _node, - '+', - _Parameter(codec.encode(n), storageType: codec.sqlType), - ), - codec, - ); - Expr minus(T n) => Expr._( - _Binary( - _node, - '-', - _Parameter(codec.encode(n), storageType: codec.sqlType), - ), - codec, - ); - Expr times(T n) => Expr._( - _Binary( - _node, - '*', - _Parameter(codec.encode(n), storageType: codec.sqlType), - ), - codec, - ); - Expr sum() => Expr._(_Function('SUM', [_node]), codec.nullable()); - Expr average() => - Expr._(_Function('AVG', [_node]), Codecs.real.nullable()); -} - -final class OrderTerm { - final Expr expression; - final bool descending; - final NullOrder? nulls; - const OrderTerm._(this.expression, this.descending, [this.nulls]); - String _write(_Writer writer) { - final text = expression._node.write(writer); - if (writer.mysql && nulls != null) { - return '($text IS NULL) ${nulls == NullOrder.first ? 'DESC' : 'ASC'}, ' - '${expression._node.write(writer)} ${descending ? 'DESC' : 'ASC'}'; - } - return '$text$_suffix'; - } - - String get _suffix => - ' ${descending ? 'DESC' : 'ASC'}' - '${nulls == null ? '' : ' NULLS ${nulls!.name.toUpperCase()}'}'; -} - -String _projection(Expr expression, _Writer writer) { - final text = expression._node.write(writer); - // Preserve JSON null separately from SQL NULL through native driver decoders. - return writer.mysql && expression.codec.sqlType == 'json' - ? 'CAST($text AS CHAR CHARACTER SET utf8mb4)' - : text; -} diff --git a/lib/src/sqlite/decimal.dart b/lib/src/sqlite/decimal.dart index d665748f..570179da 100644 --- a/lib/src/sqlite/decimal.dart +++ b/lib/src/sqlite/decimal.dart @@ -1,6 +1,8 @@ -part of 'functions.dart'; +import 'package:sqlite3/common.dart' as native; -void _registerDecimals(native.CommonDatabase db) { +import '../../values.dart'; + +void registerDecimalFunctions(native.CommonDatabase db) { db.createAggregateFunction( functionName: 'orm_decimal_avg_v1', argumentCount: const native.AllowedArgumentCount(3), diff --git a/lib/src/sqlite/driver.dart b/lib/src/sqlite/driver.dart new file mode 100644 index 00000000..f159b42a --- /dev/null +++ b/lib/src/sqlite/driver.dart @@ -0,0 +1,55 @@ +import '../../driver.dart'; +import 'options.dart'; +import 'unsupported.dart' + if (dart.library.io) 'native.dart' + if (dart.library.js_interop) 'web.dart'; + +/// Owns one SQLite connection and serializes complete connection leases. +/// +/// Native execution runs in an isolate; browser execution runs in a worker. +/// Closing drains accepted leases and then releases the database and worker. +final class SqliteDriver implements Driver { + final SqlConnection _connection; + final Future Function() _shutdown; + @override + final Capabilities capabilities; + Future _tail = Future.value(); + Future? _closing; + bool _closed = false; + SqliteDriver._(this._connection, this.capabilities, this._shutdown); + + /// Opens the configured database and checks SQLite 3.35 or newer. + /// + /// Unsupported storage choices and platform capabilities fail before a usable + /// driver is returned. Inspect [capabilities] for cancellation support. + static Future open(SqliteOptions options) async { + final opened = await connectSqlite(options); + if (opened.version < 3035000) { + await opened.close(); + throw const OrmException( + 'CAPABILITY.VERSION', + 'SQLite 3.35 or newer is required.', + ); + } + return SqliteDriver._(opened.connection, opened.capabilities, opened.close); + } + + /// Runs [action] exclusively; the next lease starts after its future settles. + @override + Future run(Future Function(SqlConnection) action) { + if (_closed) { + throw const OrmException('DRIVER.CLOSED', 'SQLite driver is closed.'); + } + // Queue the whole lease: no caller can enter another caller's transaction. + final result = _tail.then((_) => action(_connection)); + _tail = result.then((_) {}, onError: (Object _, StackTrace _) {}); + return result; + } + + /// Rejects new leases, drains accepted work, and releases owned resources. + @override + Future close() { + _closed = true; + return _closing ??= _tail.then((_) => _shutdown()); + } +} diff --git a/lib/src/sqlite/failure.dart b/lib/src/sqlite/failure.dart index 3eeafd0c..26126f83 100644 --- a/lib/src/sqlite/failure.dart +++ b/lib/src/sqlite/failure.dart @@ -1,5 +1,6 @@ import '../../driver.dart'; +/// A SQLite error preserving both the primary and extended result codes. final class SqliteFailure implements SqlFailure { @override bool get retryTransaction => code == 5; @@ -7,9 +8,17 @@ final class SqliteFailure implements SqlFailure { bool get retryCommit => code == 5; @override bool get commitRejected => code == 5 || code == 19; + + /// Primary SQLite result code, such as BUSY (5) or CONSTRAINT (19). final int code; + + /// Extended result code identifying the specific SQLite failure. final int extendedCode; + + /// Error text returned by the SQLite engine. final String message; + + /// Preserves the engine failure without interpreting its message. const SqliteFailure(this.code, this.extendedCode, this.message); @override String toString() => 'SqliteFailure($extendedCode): $message'; diff --git a/lib/src/sqlite/functions.dart b/lib/src/sqlite/functions.dart index ef4d405e..e16b59e0 100644 --- a/lib/src/sqlite/functions.dart +++ b/lib/src/sqlite/functions.dart @@ -2,12 +2,12 @@ import 'package:sqlite3/common.dart' as native; import '../../driver.dart'; -part 'decimal.dart'; -part 'temporal.dart'; +import 'decimal.dart'; +import 'temporal.dart'; void registerSqliteFunctions(native.CommonDatabase db) { - _registerDecimals(db); - _registerTemporals(db); + registerDecimalFunctions(db); + registerTemporalFunctions(db); } List sqliteParameters(List parameters) => [ diff --git a/lib/src/sqlite/native.dart b/lib/src/sqlite/native.dart index 78f84efd..fd2873f0 100644 --- a/lib/src/sqlite/native.dart +++ b/lib/src/sqlite/native.dart @@ -1,17 +1,8 @@ -import 'dart:async'; -import 'dart:isolate'; -import 'dart:ffi' as ffi; - -import 'package:sqlite3/sqlite3.dart' as native; - import '../../driver.dart'; -import 'execution.dart'; -import 'failure.dart'; import 'opened.dart'; import 'options.dart'; -part 'native_client.dart'; -part 'native_server.dart'; +import 'native_client.dart'; Future connectSqlite(SqliteOptions options) async { if (options.path.isEmpty || @@ -22,7 +13,7 @@ Future connectSqlite(SqliteOptions options) async { 'Set nativePath for SqliteOptions.persistent, and use memory() for memory storage.', ); } - final worker = _SqliteWorker(); + final worker = SqliteWorker(); final (port, maxParameters, version) = await worker.open(options); worker.port = port; return ( diff --git a/lib/src/sqlite/native_client.dart b/lib/src/sqlite/native_client.dart index 51d644bc..0e5d5d1c 100644 --- a/lib/src/sqlite/native_client.dart +++ b/lib/src/sqlite/native_client.dart @@ -1,4 +1,12 @@ -part of 'native.dart'; +import 'dart:async'; +import 'dart:ffi' as ffi; +import 'dart:isolate'; + +import '../../driver.dart'; +import 'failure.dart'; +import 'native_protocol.dart'; +import 'native_server.dart'; +import 'options.dart'; // Resolve against the exact native asset used by package:sqlite3. Never call // an unrelated system SQLite library with this library's database pointer. @@ -8,13 +16,7 @@ part of 'native.dart'; ) external void _sqliteInterrupt(ffi.Pointer db); -final class _OpenCursor(final SqlCommand command) {} - -final class _FetchCursor(final int cursor, final int count) {} - -final class _CloseCursor(final int cursor) {} - -final class _SqliteWorker implements SqlConnection { +final class SqliteWorker implements SqlConnection { final ReceivePort _responses = ReceivePort(); final Completer<(SendPort, int, int)> _ready = Completer(); final Map> _pending = {}; @@ -87,7 +89,7 @@ final class _SqliteWorker implements SqlConnection { }); try { _isolate = await Isolate.spawn( - _sqliteMain, + runSqliteWorker, (_responses.sendPort, options), onError: _responses.sendPort, onExit: _responses.sendPort, @@ -180,7 +182,7 @@ final class _SqliteWorker implements SqlConnection { SqlCommand command, { ExecutionOptions options = const ExecutionOptions(), }) async { - final result = await _operate(_OpenCursor(command), options); + final result = await _operate(OpenSqliteCursor(command), options); return _SqliteCursor(this, result.rows.single.single as int); } @@ -200,7 +202,7 @@ final class _SqliteWorker implements SqlConnection { Future invalidate() => stop(); } -final class _SqliteCursor(final _SqliteWorker worker, final int id) +final class _SqliteCursor(final SqliteWorker worker, final int id) implements SqlCursor { bool _closed = false; @override @@ -210,13 +212,13 @@ final class _SqliteCursor(final _SqliteWorker worker, final int id) }) { if (_closed) throw const OrmException('CURSOR.CLOSED', 'Cursor has ended.'); if (count < 1) throw ArgumentError.value(count, 'count'); - return worker._operate(_FetchCursor(id, count), options); + return worker._operate(FetchSqliteCursor(id, count), options); } @override Future close() async { if (_closed) return; _closed = true; - await worker._operate(_CloseCursor(id), const ExecutionOptions()); + await worker._operate(CloseSqliteCursor(id), const ExecutionOptions()); } } diff --git a/lib/src/sqlite/native_protocol.dart b/lib/src/sqlite/native_protocol.dart new file mode 100644 index 00000000..16e3d57e --- /dev/null +++ b/lib/src/sqlite/native_protocol.dart @@ -0,0 +1,7 @@ +import '../../driver.dart'; + +final class OpenSqliteCursor(final SqlCommand command) {} + +final class FetchSqliteCursor(final int cursor, final int count) {} + +final class CloseSqliteCursor(final int cursor) {} diff --git a/lib/src/sqlite/native_server.dart b/lib/src/sqlite/native_server.dart index f67039bc..5882018b 100644 --- a/lib/src/sqlite/native_server.dart +++ b/lib/src/sqlite/native_server.dart @@ -1,6 +1,14 @@ -part of 'native.dart'; +import 'dart:isolate'; -void _sqliteMain((SendPort, SqliteOptions) init) async { +import 'package:sqlite3/sqlite3.dart' as native; + +import '../../driver.dart'; +import 'execution.dart'; +import 'failure.dart'; +import 'native_protocol.dart'; +import 'options.dart'; + +void runSqliteWorker((SendPort, SqliteOptions) init) async { final (response, options) = init; native.Database? db; final commands = ReceivePort(); @@ -41,13 +49,13 @@ void _sqliteMain((SendPort, SqliteOptions) init) async { switch (command) { case SqlCommand(): result = executor!.execute(command); - case _OpenCursor(): + case OpenSqliteCursor(): result = SqlResult([ [executor!.openCursor(command.command)], ]); - case _FetchCursor(): + case FetchSqliteCursor(): result = executor!.fetch(command.cursor, command.count); - case _CloseCursor(): + case CloseSqliteCursor(): executor!.release(command.cursor); result = const SqlResult([]); default: diff --git a/lib/src/sqlite/options.dart b/lib/src/sqlite/options.dart index fa5f44f8..aa75595c 100644 --- a/lib/src/sqlite/options.dart +++ b/lib/src/sqlite/options.dart @@ -1,14 +1,38 @@ import 'web_build.dart'; -enum SqliteJournal { wal, delete, memory } +/// Native SQLite journal mode; browser persistence manages its own storage. +enum SqliteJournal { + /// Uses a write-ahead log beside the database file. + wal, + /// Deletes the rollback journal after each completed transaction. + delete, + + /// Keeps the rollback journal in memory. + memory, +} + +/// Chooses an in-memory, native-file, or browser-persistent SQLite database. +/// +/// Use [SqliteOptions.memory] on any platform and [SqliteOptions.persistent] for +/// shared application configuration across native and browser deployments. final class SqliteOptions { /// Native file path. Persistent browser databases use [name] instead. final String path; + + /// Browser persistence name; must contain only letters, digits, `_`, `-`, `.`. final String? name; + + /// Requested native journal mode; `null` preserves the existing mode. final SqliteJournal? journal; + + /// Whether a native file is opened without write access. final bool readOnly; + + /// Native wait time for a locked database before returning SQLite BUSY. final Duration busyTimeout; + + /// Browser worker and asset configuration. final SqliteWebOptions web; /// A native file. Web callers should use [SqliteOptions.persistent]. @@ -20,6 +44,7 @@ final class SqliteOptions { readOnly = false, web = const SqliteWebOptions(); + /// Opens an existing native file without changing its journal mode. const SqliteOptions.readOnly( this.path, { this.busyTimeout = const Duration(seconds: 5), @@ -28,6 +53,7 @@ final class SqliteOptions { readOnly = true, web = const SqliteWebOptions(); + /// Creates an isolated database discarded when its driver closes. const SqliteOptions.memory({ this.busyTimeout = const Duration(seconds: 5), this.web = const SqliteWebOptions(), @@ -58,11 +84,17 @@ final class SqliteWebOptions { /// Overrides are relative to the document's HTML base, not [assetBase]. final Uri? wasm; + + /// Worker script override, resolved against the HTML base URI. final Uri? worker; /// Subresource integrity for the SQLite module. Override for a custom build. final String wasmIntegrity; + + /// Deadline for the worker handshake and database initialization. final Duration openTimeout; + + /// Uses the matching packaged worker and SQLite module unless overridden. const SqliteWebOptions({ this.assetBase, this.wasm, diff --git a/lib/src/sqlite/temporal.dart b/lib/src/sqlite/temporal.dart index c269cd37..a40f32af 100644 --- a/lib/src/sqlite/temporal.dart +++ b/lib/src/sqlite/temporal.dart @@ -1,6 +1,8 @@ -part of 'functions.dart'; +import 'package:sqlite3/common.dart' as native; -void _registerTemporals(native.CommonDatabase db) { +import '../../values.dart'; + +void registerTemporalFunctions(native.CommonDatabase db) { Object round(Object value, String kind, int digits) => switch (kind) { 'time' => Codecs.time.decode(value).withPrecision(digits), 'local_datetime' => diff --git a/lib/src/sqlite/web.dart b/lib/src/sqlite/web.dart index cf98c8a0..15074a81 100644 --- a/lib/src/sqlite/web.dart +++ b/lib/src/sqlite/web.dart @@ -1,16 +1,13 @@ -import 'dart:async'; import 'dart:js_interop'; import 'package:web/web.dart' as web; import '../../driver.dart'; -import 'failure.dart'; import 'opened.dart'; import 'options.dart'; import 'web_build.dart'; -import 'web_wire.dart'; -part 'web_client.dart'; +import 'web_client.dart'; Future connectSqlite(SqliteOptions options) async { final config = options.web; @@ -59,7 +56,7 @@ Future connectSqlite(SqliteOptions options) async { 'SQLite worker must be same-origin HTTP(S); WASM must have an HTTP(S) URL.', ); } - final connection = _WebConnection(web.Worker(workerUri.toString().toJS)); + final connection = WebConnection(web.Worker(workerUri.toString().toJS)); try { Future> initialize() async { final hello = await connection.request('hello'); diff --git a/lib/src/sqlite/web_build.dart b/lib/src/sqlite/web_build.dart index e997dd3f..3add1cc6 100644 --- a/lib/src/sqlite/web_build.dart +++ b/lib/src/sqlite/web_build.dart @@ -1,11 +1,11 @@ // Generated by tool/build_sqlite_web.dart. Do not edit. const sqliteWebProtocol = 1; const sqliteWorkerBuild = - '54c9f756d7c963bdcf116d51a4a5380bdc43704ebdef02add66417747c9d76eb'; + '811c9351ca7a8a06979b43b9b68aaf97fac2c0c721c506e574d7897eed0693d6'; const sqliteWorkerFile = - 'worker-50917250e3562557e14927df4611d5d5a01a3dbc9bb641a0fa517a3d784e3819.js'; + 'worker-0c84ece1c73ab998fbc2c37fe03c3ea9138bf2a717039553dcabf80481929eb1.js'; const sqliteWorkerSha256 = - '50917250e3562557e14927df4611d5d5a01a3dbc9bb641a0fa517a3d784e3819'; + '0c84ece1c73ab998fbc2c37fe03c3ea9138bf2a717039553dcabf80481929eb1'; const sqliteWasmFile = 'sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm'; const sqliteWasmSha256 = diff --git a/lib/src/sqlite/web_client.dart b/lib/src/sqlite/web_client.dart index a8928742..3be460c1 100644 --- a/lib/src/sqlite/web_client.dart +++ b/lib/src/sqlite/web_client.dart @@ -1,12 +1,19 @@ -part of 'web.dart'; +import 'dart:async'; +import 'dart:js_interop'; -final class _WebConnection implements SqlConnection { +import 'package:web/web.dart' as web; + +import '../../driver.dart'; +import 'failure.dart'; +import 'web_wire.dart'; + +final class WebConnection implements SqlConnection { final web.Worker worker; final Map> _pending = {}; int _serial = 0; bool _closed = false, _busy = false; bool? _active = false; - _WebConnection(this.worker) { + WebConnection(this.worker) { worker.onmessage = ((web.MessageEvent event) { try { final message = event.data! as JSArray; @@ -140,7 +147,7 @@ final class _WebConnection implements SqlConnection { ); } -final class _WebCursor(final _WebConnection connection, final int id) +final class _WebCursor(final WebConnection connection, final int id) implements SqlCursor { bool _closed = false; @override diff --git a/lib/src/sqlite/web_worker.dart b/lib/src/sqlite/web_worker.dart new file mode 100644 index 00000000..947ba94e --- /dev/null +++ b/lib/src/sqlite/web_worker.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:sqlite3/wasm.dart' as sqlite; +import 'package:web/web.dart' as web; + +import '../../driver.dart'; +import 'execution.dart'; +import 'web_build.dart'; +import 'web_wire.dart'; + +/// Serves SQLite requests inside a dedicated Web worker. +/// +/// Call once from a custom worker entrypoint. Normal applications use the +/// verified worker bundled by the package instead of starting this directly. +void runSqliteWebWorker() { + final self = globalContext as web.DedicatedWorkerGlobalScope; + final server = _WorkerDatabase(); + Future tail = Future.value(); + self.onmessage = ((web.MessageEvent event) { + tail = tail.then((_) async { + final message = event.data! as JSArray; + final id = message[0]; + try { + final result = await server.handle( + (message[1]! as JSString).toDart, + message[2], + ); + self.postMessage([id, true.toJS, server.active?.toJS, result].toJS); + } catch (error) { + final JSArray details; + if (error is sqlite.SqliteException) { + details = [ + 'sqlite'.toJS, + error.message.toJS, + error.resultCode.toJS, + error.extendedResultCode.toJS, + ].toJS; + } else { + details = [ + (error is OrmException ? error.code : 'DRIVER.SQLITE').toJS, + error.toString().toJS, + ].toJS; + } + self.postMessage([id, false.toJS, server.active?.toJS, details].toJS); + } + }); + }).toJS; +} + +final class _WorkerDatabase { + sqlite.WasmSqlite3? engine; + sqlite.CommonDatabase? db; + sqlite.SimpleOpfsFileSystem? opfs; + sqlite.InMemoryFileSystem? memory; + SqliteExecutor? executor; + bool? get active => db == null ? null : !db!.autocommit; + + Future handle(String operation, JSAny? payload) async { + if (operation == 'hello') { + return [sqliteWebProtocol.toJS, sqliteWorkerBuild.toJS].toJS; + } + if (operation == 'open') { + if (engine != null) { + throw const OrmException('DRIVER.OPEN', 'Worker is already open.'); + } + final options = payload! as JSArray; + final wasm = (options[0]! as JSString).toDart; + final storage = (options[1]! as JSString).toDart; + try { + final web.Response response; + try { + response = await (globalContext as web.DedicatedWorkerGlobalScope) + .fetch( + wasm.toJS, + web.RequestInit(integrity: (options[3]! as JSString).toDart), + ) + .toDart; + if (!response.ok) throw StateError('HTTP ${response.status}'); + } catch (e) { + throw OrmException( + 'DRIVER.ASSET', + 'SQLite WASM fetch or integrity check failed.', + cause: e, + ); + } + final bytes = (await response.arrayBuffer().toDart).toDart; + final sqlite3 = engine = await sqlite.WasmSqlite3.load( + Uint8List.view(bytes), + ); + if (storage == 'opfs') { + opfs = await sqlite.SimpleOpfsFileSystem.loadFromStorage( + 'dart-orm/${(options[2]! as JSString).toDart}', + ); + sqlite3.registerVirtualFileSystem(opfs!, makeDefault: true); + } else if (storage == 'memory') { + memory = sqlite.InMemoryFileSystem(); + sqlite3.registerVirtualFileSystem(memory!, makeDefault: true); + } else { + throw const OrmException( + 'DRIVER.STORAGE', + 'Unknown browser storage.', + ); + } + final opened = db = sqlite3.open( + storage == 'memory' ? ':memory:' : '/database', + ); + executor = SqliteExecutor(opened); + // Sync OPFS handles own the database exclusively; a busy wait cannot + // coordinate another owner. Keep durable rollback-journal storage. + final maxParameters = configureSqlite( + opened, + journal: storage == 'memory' ? 'memory' : 'delete', + busyTimeout: Duration.zero, + fullSync: true, + ); + return [sqlite3.version.versionNumber.toJS, maxParameters.toJS].toJS; + } catch (_) { + close(); + rethrow; + } + } + final opened = db; + if (opened == null) { + throw const OrmException('DRIVER.CLOSED', 'SQLite worker is not open.'); + } + switch (operation) { + case 'execute': + return sqliteWebResult( + executor!.execute(sqliteWebDartCommand(payload)), + ); + case 'cursor': + return executor!.openCursor(sqliteWebDartCommand(payload)).toJS; + case 'fetch': + final parts = payload! as JSArray; + return sqliteWebResult( + executor!.fetch( + (parts[0]! as JSNumber).toDartInt, + (parts[1]! as JSNumber).toDartInt, + ), + ); + case 'release': + executor!.release((payload! as JSNumber).toDartInt); + return null; + case 'close': + close(); + return null; + default: + throw const OrmException( + 'DRIVER.PROTOCOL', + 'Unknown SQLite worker command.', + ); + } + } + + void close() { + if (executor != null) { + executor!.close(); + } else { + db?.close(); + } + executor = null; + db = null; + if (opfs case final fs?) { + engine?.unregisterVirtualFileSystem(fs); + fs.close(); + } + if (memory case final fs?) engine?.unregisterVirtualFileSystem(fs); + opfs = null; + memory = null; + } +} diff --git a/lib/src/table.dart b/lib/src/table.dart deleted file mode 100644 index cbc941be..00000000 --- a/lib/src/table.dart +++ /dev/null @@ -1,96 +0,0 @@ -part of '../sql.dart'; - -/// A table occurrence has nominal identity, even when two row records have the -/// same structural Dart type or a query joins the same physical table twice. -final class TableRef { - final TableSchema schema; - TableRef(this.schema); -} - -abstract class Fields { - final TableRef table; - const Fields(this.table); - Field column(Column column) => Field._(table, column); - ReadField readColumn(Column column) => ReadField._(table, column); -} - -base class ReadField extends Expr { - final TableRef table; - final Column definition; - ReadField._(this.table, this.definition) - : super._(_ColumnNode(table, definition.name), definition.codec); -} - -final class Field extends ReadField { - Field._(super.table, super.definition) : super._() { - if (definition.computed != null) { - throw const OrmException( - 'COLUMN.READ_ONLY', - 'Use readColumn for a computed column.', - ); - } - } - Assignment set(T value) => - _assign(_Parameter(codec.encode(value), storageType: codec.sqlType)); - Assignment setExpression(Expr expression) => _assign(expression._node); - Assignment _assign(_Node node) => Assignment._( - this, - definition.decimalPrecision == null - ? definition.temporalPrecision == null - ? node - : _TemporalCast( - node, - definition.codec.sqlType, - definition.temporalPrecision!, - columnAssignment: true, - ) - : _DecimalCast( - node, - definition.decimalPrecision!, - definition.decimalScale ?? 0, - columnAssignment: true, - ), - ); - Assignment defaultValue() => Assignment._(this, null); -} - -extension NumericField on Field { - Assignment increment(T amount) => setExpression(plus(amount)); - Assignment decrement(T amount) => setExpression(minus(amount)); -} - -final class Table { - final TableSchema schema; - final F Function(TableRef) createFields; - final Selection Function(F) selectRow; - const Table(this.schema, this.createFields, this.selectRow); - TableAlias alias() => TableAlias._(this); -} - -sealed class Change { - const Change(); - const factory Change.keep() = _Keep; - const factory Change.set(T value) = _Set; - const factory Change.defaultValue() = _Default; -} - -final class _Keep extends Change { - const _Keep(); -} - -final class _Set extends Change { - final T value; - const _Set(this.value); -} - -final class _Default extends Change { - const _Default(); -} - -extension ChangeField on Field { - List change(Change change) => switch (change) { - _Keep() => [], - _Set(:final value) => [set(value)], - _Default() => [defaultValue()], - }; -} diff --git a/lib/src/temporal_sql.dart b/lib/src/temporal_sql.dart deleted file mode 100644 index 6154f403..00000000 --- a/lib/src/temporal_sql.dart +++ /dev/null @@ -1,79 +0,0 @@ -part of '../sql.dart'; - -final class _TemporalNode(final _Node child, final String kind) extends _Node { - @override - String writeSql(_Writer w) { - if (!w.temporal) { - throw const OrmException( - 'CAPABILITY.TEMPORAL', - 'This driver does not provide temporal values.', - ); - } - final text = child.write(w); - return w.dialect == SqlDialect.sqlite - ? '($text COLLATE "orm_${kind}_v1")' - : text; - } -} - -void _checkTemporalPrecision(int digits) { - if (digits < 0 || digits > 6) throw RangeError.range(digits, 0, 6, 'digits'); -} - -final class _TemporalCast( - final _Node child, - final String kind, - final int digits, { - final bool columnAssignment = false, -}) extends _Node { - @override - String writeSql(_Writer w) { - _checkTemporalPrecision(digits); - if (!w.temporal) { - throw const OrmException( - 'CAPABILITY.TEMPORAL', - 'This driver does not provide temporal values.', - ); - } - final type = switch (kind) { - 'time' => 'TIME($digits) WITHOUT TIME ZONE', - 'local_datetime' => 'TIMESTAMP($digits) WITHOUT TIME ZONE', - 'instant' => 'TIMESTAMPTZ($digits)', - _ => throw ArgumentError('Temporal precision requires temporal storage.'), - }; - final sql = child.write(w); - if (w.mysql) { - // Storage coercion follows the declared database column. Expression - // rounding has a stronger cross-engine contract and stays explicit. - if (columnAssignment) return sql; - throw const OrmException( - 'CAPABILITY.TEMPORAL_PRECISION', - 'MySQL/MariaDB temporal rounding differs; declare column precision or use explicit SQL.', - ); - } - return w.dialect == SqlDialect.postgres - ? 'CAST($sql AS $type)' - : "orm_temporal_cast_v1($sql, '$kind', $digits)"; - } -} - -extension TimeExpression on Expr { - Expr withPrecision(int digits) { - _checkTemporalPrecision(digits); - return Expr._(_TemporalCast(_node, 'time', digits), codec); - } -} - -extension LocalDateTimeExpression on Expr { - Expr withPrecision(int digits) { - _checkTemporalPrecision(digits); - return Expr._(_TemporalCast(_node, 'local_datetime', digits), codec); - } -} - -extension InstantExpression on Expr { - Expr withPrecision(int digits) { - _checkTemporalPrecision(digits); - return Expr._(_TemporalCast(_node, 'instant', digits), codec); - } -} diff --git a/lib/src/codec.dart b/lib/src/values/codec.dart similarity index 70% rename from lib/src/codec.dart rename to lib/src/values/codec.dart index 1c5e3f33..f5f62bf6 100644 --- a/lib/src/codec.dart +++ b/lib/src/values/codec.dart @@ -1,21 +1,35 @@ -part of '../values.dart'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'decimal.dart'; +import 'temporal.dart'; /// A non-SQL-null JSON document. Its value may itself be JSON null. /// Drivers use this envelope for parsed JSON, including JSON string scalars. final class SqlJson { + /// The decoded JSON value, including JSON `null`. final Object? value; + + /// Wraps a JSON document without treating JSON `null` as SQL NULL. const SqlJson(this.value); } /// Explicit floating-point SQL input. JavaScript cannot distinguish an /// integral double from int by runtime type; this preserves the SQL intention. final class SqlReal { + /// The value to bind as floating-point SQL input. final double value; + + /// Preserves floating-point intent even for an integral value on JavaScript. const SqlReal(this.value); } -/// Storage and Dart values meet only at this boundary. +/// Converts a typed Dart value to database storage and decodes it on reads. +/// +/// A codec defines a storage family and symmetric conversion functions. Use +/// [nullable] for SQL NULL and [map] for domain-specific Dart types. final class Codec { + /// Logical storage family used when rendering and binding SQL. final String sqlType; // Nullable wrappers share a comparison identity, while remaining distinct // from a codec whose own decoder handles SQL NULL. @@ -28,20 +42,35 @@ final class Codec { final T Function(Object? value) _decode; final Object? Function(T value) _encode; + /// Creates an explicit storage codec; conversion errors propagate to callers. const Codec(this.sqlType, this._decode, this._encode) : _identity = null; Codec._nullable(this.sqlType, this._decode, this._encode, this._identity); + + /// Stores a domain value as SQL text. const Codec.text(this._decode, String Function(T value) encode) : _identity = null, sqlType = 'text', _encode = encode; + + /// Stores a domain value as a SQL integer. const Codec.integer(this._decode, int Function(T value) encode) : _identity = null, sqlType = 'integer', _encode = encode; + + /// Converts a raw stored value into [T]. T decode(Object? value) => _decode(value); + + /// Converts [value] into a value supported by the SQL driver. Object? encode(T value) => _encode(value); + + /// Whether [T] permits a null Dart result. bool get acceptsNull => null is T; + /// Handles SQL NULL before delegating non-null values to this codec. + /// + /// Nullable wrappers share a comparison identity with each other, distinct + /// from a decoder that handles SQL NULL itself. Codec nullable() => Codec._nullable( sqlType, (value) => value == null ? null : decode(value), @@ -49,6 +78,7 @@ final class Codec { _identity ?? (this, #nullable), ); + /// Adds domain conversions while preserving this codec's storage family. Codec map(R Function(T) from, T Function(R) to) => Codec( sqlType, (value) => from(decode(value)), @@ -56,7 +86,9 @@ final class Codec { ); } +/// Built-in conversions for SQL scalar values, JSON, and exact numeric types. abstract final class Codecs { + /// Calendar dates without timezones. static const date = Codec('date', _decodeDate, _encodeDate); static LocalDate _decodeDate(Object? value) => switch (value) { LocalDate() => value, @@ -64,6 +96,8 @@ abstract final class Codecs { _ => throw const FormatException('Expected a local date or date text.'), }; static String _encodeDate(LocalDate value) => value.toString(); + + /// Wall-clock times with microsecond resolution, including 24:00. static const time = Codec('time', _decodeTime, _encodeTime); static LocalTime _decodeTime(Object? value) => switch (value) { LocalTime() => value, @@ -71,6 +105,8 @@ abstract final class Codecs { _ => throw const FormatException('Expected a local time or time text.'), }; static String _encodeTime(LocalTime value) => value.toString(); + + /// Local calendar timestamps with no implied UTC instant. static const localDateTime = Codec( 'local_datetime', _decodeLocalDateTime, @@ -84,6 +120,8 @@ abstract final class Codecs { ), }; static String _encodeLocalDateTime(LocalDateTime value) => value.toString(); + + /// Exact finite decimals encoded as text without a floating-point conversion. static const decimal = Codec( 'decimal', _decodeDecimal, @@ -122,6 +160,7 @@ abstract final class Codecs { ); } + /// Dart integers; textual database numerics must parse exactly as an integer. // PostgreSQL SUM(BIGINT) returns NUMERIC text. Parse exactly and reject // overflow instead of rounding through double. static final integer = Codec( @@ -129,18 +168,26 @@ abstract final class Codecs { (v) => v is int ? v : int.parse(v as String), (v) => v, ); + + /// Arbitrary-precision integers encoded as exact decimal text. static final bigint = Codec( 'bigint', (v) => v is BigInt ? v : BigInt.parse(v.toString()), (v) => v.toString(), ); + + /// Binary floating-point values with explicit SQL real intent on JavaScript. static final real = Codec( 'real', (v) => v is String ? double.parse(v) : (v as num).toDouble(), (v) => const bool.fromEnvironment('dart.library.js_interop') ? SqlReal(v) : v, ); + + /// SQL text without implicit string conversion. static final text = Codec('text', (v) => v as String, (v) => v); + + /// Booleans decoded from true/false or integer 1/0; other inputs are rejected. static final boolean = Codec( 'boolean', (v) => switch (v) { @@ -150,17 +197,27 @@ abstract final class Codecs { }, (v) => v, ); + + /// UTC instants within the common finite PostgreSQL and Dart DateTime range. static const dateTime = Codec( 'instant', - _decodeInstant, - _encodeInstant, + decodeInstant, + encodeInstant, ); + + /// Binary data as [Uint8List]. static final bytes = Codec( 'blob', (v) => v as Uint8List, (v) => v, ); + + /// Parsed JSON values; JSON null and SQL NULL both decode to Dart null. static const json = Codec('json', _decodeJson, jsonEncode); + + /// JSON documents wrapped in [SqlJson] to preserve JSON null. + /// + /// SQL NULL is rejected; use `.nullable()` when the column is nullable. static const jsonDocument = Codec( 'json', _jsonDocument, @@ -179,10 +236,18 @@ abstract final class Codecs { static String _encodeDocument(SqlJson value) => jsonEncode(value.value); } +/// An ORM failure with a stable machine-readable code and optional cause. final class OrmException implements Exception { + /// Failure identifier, such as `QUERY.SCOPE` or `SESSION.CLOSED`. final String code; + + /// Human-readable context explaining the failure. final String message; + + /// Underlying driver or runtime failure, when one is available. final Object? cause; + + /// Creates a classified failure while retaining an optional original cause. const OrmException(this.code, this.message, {this.cause}); @override String toString() => 'OrmException($code): $message'; diff --git a/lib/src/decimal.dart b/lib/src/values/decimal.dart similarity index 83% rename from lib/src/decimal.dart rename to lib/src/values/decimal.dart index fb175956..0746bc79 100644 --- a/lib/src/decimal.dart +++ b/lib/src/values/decimal.dart @@ -1,12 +1,23 @@ -part of '../values.dart'; +import 'dart:math' as math; /// Rounding is explicit; [exact] rejects a non-zero discarded remainder. enum DecimalRounding { + /// Rejects any non-zero discarded remainder. exact, + + /// Discards remaining digits without increasing magnitude. towardZero, + + /// Rounds toward negative infinity. floor, + + /// Rounds toward positive infinity. ceiling, + + /// Rounds to the nearest value, moving exact ties away from zero. halfAwayFromZero, + + /// Rounds to the nearest value, choosing an even final digit for exact ties. halfEven, } @@ -16,13 +27,23 @@ enum DecimalRounding { /// matches finite unconstrained PostgreSQL NUMERIC: at most 131072 integer /// digits and 16383 fractional digits. This does not retain display precision. final class Decimal implements Comparable { + /// Maximum count of digits to the left of the decimal point. static const maxIntegerDigits = 131072; + + /// Maximum count of digits to the right of the decimal point. static const maxFractionDigits = 16383; + + /// The canonical normalized zero. static final zero = Decimal._(BigInt.zero, 0); + + /// Normalized integer significand, without trailing decimal zeroes. final BigInt coefficient; + + /// Decimal exponent divisor: the value equals coefficient × 10^(-scale). final int scale; const Decimal._(this.coefficient, this.scale); + /// Creates coefficient × 10^(-scale), normalizing trailing zeroes. factory Decimal.fromBigInt(BigInt coefficient, {int scale = 0}) { if (coefficient == BigInt.zero) return zero; return Decimal._digits( @@ -32,6 +53,10 @@ final class Decimal implements Comparable { ); } + /// Parses finite decimal text, including scientific notation. + /// + /// Invalid syntax throws [FormatException]; unsupported magnitude or scale + /// throws [RangeError]. No intermediate floating-point value is used. factory Decimal.parse(String source) { // Bound parsing work before allocating a potentially enormous BigInt. if (source.length > maxIntegerDigits + maxFractionDigits + 32) { @@ -61,6 +86,7 @@ final class Decimal implements Comparable { r'^([+-]?)([0-9]*)(?:\.([0-9]*))?(?:[eE]([+-]?[0-9]+))?$', ); + /// Parses finite decimal text, returning null for invalid or out-of-range input. static Decimal? tryParse(String source) { try { return Decimal.parse(source); @@ -96,7 +122,10 @@ final class Decimal implements Comparable { static BigInt _power(int n) => BigInt.from(10).pow(n); + /// Returns the exact additive inverse. Decimal operator -() => Decimal._(-coefficient, scale); + + /// Adds two exact values and checks the supported result range. Decimal operator +(Decimal other) { final common = math.max(scale, other.scale); return Decimal.fromBigInt( @@ -106,7 +135,10 @@ final class Decimal implements Comparable { ); } + /// Subtracts [other] without rounding. Decimal operator -(Decimal other) => this + -other; + + /// Multiplies exactly and checks the supported result range. Decimal operator *(Decimal other) => Decimal.fromBigInt( coefficient * other.coefficient, scale: scale + other.scale, @@ -150,6 +182,7 @@ final class Decimal implements Comparable { ); } + /// Rounds to an explicit scale; the default rejects lost precision. Decimal rounded( int scale, { DecimalRounding rounding = DecimalRounding.exact, @@ -159,6 +192,7 @@ final class Decimal implements Comparable { return _quotient(coefficient, _power(this.scale - scale), scale, rounding); } + /// Throws [RangeError] if a result scale exceeds the supported finite range. static void validateScale(int scale) { if (scale < -maxIntegerDigits || scale > maxFractionDigits) { throw RangeError.range( @@ -190,6 +224,7 @@ final class Decimal implements Comparable { return result; } + /// Validates PostgreSQL-style column precision and scale declarations. static void validateDigits(int precision, int scale) { if (precision < 1 || precision > 1000 || scale < -1000 || scale > 1000) { throw ArgumentError( @@ -239,9 +274,16 @@ final class Decimal implements Comparable { ); } + /// Compares exact numeric values, independent of their stored scale. bool operator <(Decimal other) => compareTo(other) < 0; + + /// Whether this exact value is less than or equal to [other]. bool operator <=(Decimal other) => compareTo(other) <= 0; + + /// Whether this exact value is greater than [other]. bool operator >(Decimal other) => compareTo(other) > 0; + + /// Whether this exact value is greater than or equal to [other]. bool operator >=(Decimal other) => compareTo(other) >= 0; @override bool operator ==(Object other) => diff --git a/lib/src/temporal.dart b/lib/src/values/temporal.dart similarity index 62% rename from lib/src/temporal.dart rename to lib/src/values/temporal.dart index 5c35330f..69c3766d 100644 --- a/lib/src/temporal.dart +++ b/lib/src/values/temporal.dart @@ -1,5 +1,3 @@ -part of '../values.dart'; - const _microsecondsPerDay = 86400000000; int _floorDiv(int a, int b) => a ~/ b - (a < 0 && a % b != 0 ? 1 : 0); String _pad(int value, int length) => value.toString().padLeft(length, '0'); @@ -21,11 +19,26 @@ int _gregorianJulianDay(int year, int month, int day) { /// A Gregorian calendar date, without a time or timezone. /// Year 0 is 1 BC; negative years use astronomical numbering. final class LocalDate implements Comparable { + /// First supported Julian day, matching the finite PostgreSQL DATE range. static const minJulianDay = 0; + + /// Last supported Julian day for a finite calendar date. static const maxJulianDay = 2147483493; - final int year, month, day; + + /// Astronomical year; year zero corresponds to 1 BC. + final int year; + + /// Gregorian month in the range 1 through 12. + final int month; + + /// Day of the month, validated against the month and leap-year rules. + final int day; + + /// Integer calendar-day ordinal used for exact comparison and date arithmetic. final int julianDay; const LocalDate._(this.year, this.month, this.day, this.julianDay); + + /// Creates a valid Gregorian date or throws [RangeError]. factory LocalDate(int year, int month, int day) { if (year < -4713 || year > 5874897 || @@ -41,6 +54,8 @@ final class LocalDate implements Comparable { } return LocalDate._(year, month, day, ordinal); } + + /// Converts a supported Julian day ordinal to its Gregorian date. factory LocalDate.fromJulianDay(int value) { if (value < minJulianDay || value > maxJulianDay) { throw RangeError.range(value, minJulianDay, maxJulianDay, 'julianDay'); @@ -60,6 +75,8 @@ final class LocalDate implements Comparable { value, ); } + + /// Parses `YYYY-MM-DD`, optionally followed by ` BC`. factory LocalDate.parse(String text) { final match = _syntax.firstMatch(text); if (match == null || match.end != text.length) { @@ -80,6 +97,8 @@ final class LocalDate implements Comparable { r'^(-?\d{4,7})-(\d{2})-(\d{2})( BC)?$', caseSensitive: false, ); + + /// Returns null when [text] is invalid or outside the supported range. static LocalDate? tryParse(String text) { try { return LocalDate.parse(text); @@ -95,6 +114,8 @@ final class LocalDate implements Comparable { 4 || 6 || 9 || 11 => 30, _ => 31, }; + + /// Adds whole calendar days, rejecting results outside the supported range. LocalDate addDays(int days) { if (days < minJulianDay - julianDay || days > maxJulianDay - julianDay) { throw RangeError('Date addition exceeds the supported range.'); @@ -102,6 +123,7 @@ final class LocalDate implements Comparable { return LocalDate.fromJulianDay(julianDay + days); } + /// Signed number of calendar days from this date to [other]. int daysUntil(LocalDate other) => other.julianDay - julianDay; String get _civil => '${_pad(year <= 0 ? 1 - year : year, 4)}-${_pad(month, 2)}-${_pad(day, 2)}'; @@ -119,8 +141,11 @@ final class LocalDate implements Comparable { /// A wall-clock time at microsecond resolution, including the endpoint 24:00. /// The fourth constructor argument is the full fractional second (0..999999). final class LocalTime implements Comparable { + /// Microseconds since midnight, including the endpoint 86400000000. final int microseconds; const LocalTime._(this.microseconds); + + /// Creates a valid wall-clock time or throws [RangeError]. factory LocalTime( int hour, [ int minute = 0, @@ -142,12 +167,16 @@ final class LocalTime implements Comparable { ((hour * 60 + minute) * 60 + second) * 1000000 + microsecond, ); } + + /// Creates a time from microseconds since midnight, including 24:00. factory LocalTime.fromMicroseconds(int value) { if (value < 0 || value > _microsecondsPerDay) { throw RangeError.range(value, 0, _microsecondsPerDay, 'microseconds'); } return LocalTime._(value); } + + /// Parses `HH:mm[:ss[.ffffff]]` without a timezone. factory LocalTime.parse(String text) { final match = _syntax.firstMatch(text); if (match == null || match.end != text.length) { @@ -165,6 +194,8 @@ final class LocalTime implements Comparable { static final _syntax = RegExp( r'^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?)?$', ); + + /// Returns null when [text] is not a supported wall-clock time. static LocalTime? tryParse(String text) { try { return LocalTime.parse(text); @@ -180,9 +211,16 @@ final class LocalTime implements Comparable { microseconds + _temporalRoundDelta(microseconds, digits), ); + /// Hour from 0 through 24; 24 is allowed only at the end-of-day endpoint. int get hour => microseconds ~/ 3600000000; + + /// Minute within the hour, from 0 through 59. int get minute => microseconds ~/ 60000000 % 60; + + /// Second within the minute, from 0 through 59. int get second => microseconds ~/ 1000000 % 60; + + /// Full fractional second, from 0 through 999999. int get microsecond => microseconds % 1000000; @override String toString() => @@ -198,10 +236,17 @@ final class LocalTime implements Comparable { /// Calendar date and wall-clock time, with no timezone or implied UTC instant. final class LocalDateTime implements Comparable { + /// Last supported Julian day for a finite PostgreSQL local timestamp. static const maxJulianDay = 109203527; + + /// Calendar date after normalizing a 24:00 input to the next day. final LocalDate date; + + /// Wall-clock time, always earlier than 24:00 after normalization. final LocalTime time; const LocalDateTime._(this.date, this.time); + + /// Combines a date and time, normalizing 24:00 to the next midnight. factory LocalDateTime(LocalDate date, LocalTime time) { if (time.hour == 24) { date = date.addDays(1); @@ -214,6 +259,8 @@ final class LocalDateTime implements Comparable { } return LocalDateTime._(date, time); } + + /// Parses a date and time separated by a space or `T`, without a timezone. factory LocalDateTime.parse(String text) { final match = _syntax.firstMatch(text); if (match == null || match.end != text.length) { @@ -230,6 +277,8 @@ final class LocalDateTime implements Comparable { r'^(-?\d{4,7}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?)( BC)?$', caseSensitive: false, ); + + /// Returns null for invalid or out-of-range local timestamps. static LocalDateTime? tryParse(String text) { try { return LocalDateTime.parse(text); @@ -251,6 +300,7 @@ final class LocalDateTime implements Comparable { ), ); + /// Adds elapsed calendar microseconds without applying timezone rules. LocalDateTime add(Duration duration) { final days = _floorDiv(duration.inMicroseconds, _microsecondsPerDay); final ticks = @@ -290,6 +340,7 @@ int _temporalRoundDelta(int ticks, int digits, {bool beforeEpoch = false}) { : -remainder; } +/// Explicit precision conversion for resolved UTC instants. extension InstantPrecision on DateTime { /// Round a resolved UTC instant using PostgreSQL TIMESTAMPTZ(p) rules. DateTime withPrecision(int digits) { @@ -318,3 +369,125 @@ extension InstantPrecision on DateTime { ); } } + +// Intersection of PostgreSQL's finite lower bound and Dart DateTime's range. +const _minInstantMicros = -210866803200000000; +const _maxInstantMicros = 8640000000000000000; +const _unixEpochJulian = 2440588; +const _maxInstantJulian = 102440588; + +DateTime decodeInstant(Object? value) => switch (value) { + DateTime() => _checkedInstant(value), + String() => _parseInstant(value), + _ => throw const FormatException('Expected a UTC instant or timestamp text.'), +}; + +DateTime _checkedInstant(DateTime value) { + final utc = value.toUtc(), millis = value.millisecondsSinceEpoch; + // Epoch milliseconds fit JavaScript's exact integer range; total microseconds + // do not. DateTime stores the remaining microsecond component separately. + if (millis < _minInstantMicros ~/ 1000 || + millis > _maxInstantMicros ~/ 1000 || + millis == _maxInstantMicros ~/ 1000 && utc.microsecond != 0) { + throw const FormatException( + 'Instant exceeds the common DateTime/PostgreSQL range.', + ); + } + return utc; +} + +String encodeInstant(DateTime value) { + final utc = _checkedInstant(value); + final date = LocalDate(utc.year, utc.month, utc.day); + final time = LocalTime( + utc.hour, + utc.minute, + utc.second, + utc.millisecond * 1000 + utc.microsecond, + ); + return '${date._civil} $time+00${date.year <= 0 ? ' BC' : ''}'; +} + +final _instantSyntax = RegExp( + r'^(-?\d{4,7})-(\d{2})-(\d{2})[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?)(Z|[+-]\d{2}(?::?\d{2})?(?::?\d{2})?)?( BC)?$', + caseSensitive: false, +); + +DateTime _parseInstant(String text) { + final match = _instantSyntax.firstMatch(text); + if (match == null || match.end != text.length) { + throw const FormatException( + 'Expected a timestamp with at most six fractional digits.', + ); + } + try { + var year = int.parse(match[1]!); + if (match[6] != null) { + if (year <= 0) { + throw const FormatException('BC requires a positive era year.'); + } + year = 1 - year; + } + final month = int.parse(match[2]!), day = int.parse(match[3]!); + if (month < 1 || + month > 12 || + day < 1 || + day > LocalDate._monthDays(year, month)) { + throw const FormatException('Invalid calendar date in instant.'); + } + final zone = match[5]; + var offset = 0; + if (zone != null && zone.toUpperCase() != 'Z') { + final digits = zone.substring(1).replaceAll(':', ''); + final hours = int.parse(digits.substring(0, 2)); + final minutes = digits.length >= 4 + ? int.parse(digits.substring(2, 4)) + : 0; + final seconds = digits.length == 6 + ? int.parse(digits.substring(4, 6)) + : 0; + if (hours > 15 || minutes > 59 || seconds > 59) { + throw const FormatException('Invalid UTC offset.'); + } + offset = + ((hours * 60 + minutes) * 60 + seconds) * + 1000000 * + (zone[0] == '-' ? -1 : 1); + } + // Zone-less database storage is explicitly UTC (SQLite CURRENT_TIMESTAMP), + // independent of the process's local timezone. Validate before multiplying + // days into int64 microseconds, including offset crossings at either bound. + final time = LocalTime.parse(match[4]!).microseconds - offset; + final ordinal = + _gregorianJulianDay(year, month, day) + + _floorDiv(time, _microsecondsPerDay); + final within = time % _microsecondsPerDay; + if (ordinal < 0 || + ordinal > _maxInstantJulian || + ordinal == _maxInstantJulian && within != 0) { + throw const FormatException( + 'Instant exceeds the common DateTime/PostgreSQL range.', + ); + } + if (const bool.fromEnvironment('dart.library.js_interop')) { + final date = LocalDate.fromJulianDay(ordinal); + final clock = LocalTime.fromMicroseconds(within); + return DateTime.utc( + date.year, + date.month, + date.day, + clock.hour, + clock.minute, + clock.second, + clock.microsecond ~/ 1000, + clock.microsecond % 1000, + ); + } + return DateTime.fromMicrosecondsSinceEpoch( + (ordinal - _unixEpochJulian) * _microsecondsPerDay + within, + isUtc: true, + ); + } on RangeError { + throw const FormatException('Invalid timestamp components.'); + } +} diff --git a/lib/values.dart b/lib/values.dart index 321a55b3..660ed6e0 100644 --- a/lib/values.dart +++ b/lib/values.dart @@ -1,11 +1,24 @@ -/// Database value types and explicit Dart/storage codecs. No database connection required. +/// Exact numeric values, calendar values, and explicit storage codecs. +/// +/// Use [Codecs] for built-in conversions, or [Codec.map] to store an application +/// value through an existing codec. No database connection is required. +/// +/// {@category Values} +/// {@canonicalFor codec.SqlJson} +/// {@canonicalFor codec.SqlReal} +/// {@canonicalFor codec.Codec} +/// {@canonicalFor codec.Codecs} +/// {@canonicalFor codec.OrmException} +/// {@canonicalFor decimal.Decimal} +/// {@canonicalFor decimal.DecimalRounding} +/// {@canonicalFor temporal.LocalDate} +/// {@canonicalFor temporal.LocalTime} +/// {@canonicalFor temporal.LocalDateTime} +/// {@canonicalFor temporal.InstantPrecision} library; -import 'dart:convert'; -import 'dart:math' as math; -import 'dart:typed_data'; - -part 'src/codec.dart'; -part 'src/decimal.dart'; -part 'src/instant.dart'; -part 'src/temporal.dart'; +export 'src/values/codec.dart' + show SqlJson, SqlReal, Codec, Codecs, OrmException; +export 'src/values/decimal.dart' show Decimal, DecimalRounding; +export 'src/values/temporal.dart' + show LocalDate, LocalTime, LocalDateTime, InstantPrecision; diff --git a/pubspec.lock b/pubspec.lock index 06d19ac5..b654d5b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -306,7 +306,7 @@ packages: source: hosted version: "0.12.20" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" diff --git a/pubspec.yaml b/pubspec.yaml index a98b20b8..0cca39ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: orm description: Typed relational data access for Dart, from declaration to migration. -version: 6.0.0-beta.1 +version: 6.0.0-beta.2 repository: https://github.com/medz/dart-orm issue_tracker: https://github.com/medz/dart-orm/issues documentation: https://github.com/medz/dart-orm/tree/main/doc @@ -17,6 +17,7 @@ dependencies: build: ^4.0.11 crypto: ^3.0.7 dart_style: ^3.1.13 + meta: ^1.19.0 mysql_client_plus: ^0.1.3 path: ^1.9.1 postgres: ^3.5.12 diff --git a/research/api-correctness-audit.md b/research/api-correctness-audit.md deleted file mode 100644 index 38d2af86..00000000 --- a/research/api-correctness-audit.md +++ /dev/null @@ -1,78 +0,0 @@ -# Public API and correctness audit - -Scope: the `next` implementation at `6d1ba30`, followed by the changes in this -review. This is a source and behavior audit of entrypoints, generation, query -composition, decoding, writes, sessions, drivers and migration boundaries. - -## Findings and changes - -| Finding | Consequence | Correction and evidence | -| --- | --- | --- | -| Subqueries and CTEs checked dialect but not owning session | A root, expired or other database's query could silently execute inside another transaction/connection | The SQL writer carries the owning `Database`; every nested query checks identity. `query_boundary_test` exercises rejection before SQL and valid same-session composition | -| Optional selection disabled nullability validation for every joined table | One alias's presence could incorrectly justify decoding a different absent alias as required | Selection plans retain the exact presence guards of each required expression; nested guards remain composable | -| Mutation validation omitted aggregate/window predicates and assignments | Invalid typed SQL reached the database and could poison a transaction | Mutation compilation rejects invalid placement; scalar subqueries remain available | -| Empty RETURNING and joined relation RETURNING lacked a direct contract check | Invalid SQL or unrelated alias errors instead of a useful ORM error | Reject empty projections and all relationship loading in RETURNING before execution | -| A second close() returned before the first shutdown drained its lease | Awaiting shutdown did not consistently imply resource cleanup | All close callers await the same shutdown future; real SQLite/PostgreSQL tests hold and release a pending lease | -| One throwing cancellation listener blocked the remaining listeners | Running operations could miss a requested cancellation | Isolate listener failures and preserve idempotence; regression covers subsequent and already-cancelled listeners | -| Generator imposed both engines' computed-column restrictions | Valid SQLite virtual indexes and PostgreSQL stored computed primary keys could not be declared | Generation checks structure; selecting a migration engine checks native capabilities. `schema_boundary_test` executes both cases on real databases | -| Catalog import retained the same portability restriction and a separate reserved-name list | Valid native tables could not round-trip through import and generation | Preserve the inspected engine's computed keys; share Database member names across declaration, named-query and import readers. Native import → generate → verify cases cover both engines | -| Schema validation missed malformed keys, index collisions, identifier spelling and target case rules | Invalid schemas failed late; case rules were inconsistently applied | Validate names/local keys and shared table/index names; resolve SQLite case and PostgreSQL identifier length only for that target | -| Generated extension getters could shadow Database members; generated symbols could collide | Accepted declarations produced unusable getters or uncompilable clients | Reject the declaration with a generation error and retain physical names through `table:`/`ColumnName` | -| Named SQL silently accepted some storage-only annotations | Defaults/computation/precision annotations appeared to configure a query result but were discarded | Reject all storage-only metadata; named results retain codec/column-name annotations | -| Generated clients did not export their row aliases; generated schema lists were mutable | Additional model imports and mutations could break the declaration/runtime contract | Export only row aliases, keep declaration objects private to the schema import, and freeze `appSchema` | -| Generator build factories were also public through generate.dart | Two tooling entrypoints exposed the same implementation hooks | Explicit generation facade; build factories live behind builder.dart. Migration entrypoint exports its required schema primitives | -| README still suggested changing an environment variable switched migration engines | Contradicted the fixed-engine history model | One SQLite onboarding path and an explicit explanation of separate histories | - -The initial six-case SQLite reproduction passed one preservation case and failed -five boundary cases. After the runtime correction, the related SQLite query, -relation, selection and UNION suites passed 39 cases. Full final validation is -recorded below after all generation and documentation changes. - -## Decisions retained after review - -- One package. `orm.dart` contains portable runtime code; database entrypoints - re-export it. Driver/codec/manual-table interfaces are intentional advanced API, - not extra application layers. SQL AST and decoding internals remain private. -- One query belongs to one `Database` view. Queries are immutable descriptions; - get/execute/stream perform work. A transaction callback receives a borrowed view. - Helpers must receive that view; separate calls on a captured root remain separate - operations. No ambient transaction context is introduced to hide ownership. -- A Record describes data shape. Table and alias objects carry SQL identity. - Relationship selection is explicit; no lazy object graph or hidden writes. -- Current declarations and generated clients can evolve. Saved Dart migrations - retain their historical schema, selected engine, fixed checksums and imports. -- Reviewed raw SQL is an escape hatch. The ORM does not claim to parse arbitrary - raw expressions or prove their result codec, external effects or deployment safety. -- The apparent `_withConflict`/batch-row issue was not reachable through the public - API: BatchInsert has no conflict method and uses its own chunk execution path. - It is not reported as a confirmed data-loss bug or used to add a new feature. -- Record field rename limitations, platform support, retry/unknown-COMMIT rules, - watch invalidation boundaries and backfill recovery remain explicit contracts. - -## Database references - -PostgreSQL quoted identifiers are case-sensitive and normally limited to 63 bytes; -the ORM rejects oversized explicit names instead of relying on truncation. -[PostgreSQL 18 lexical structure](https://www.postgresql.org/docs/18/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS). -Computed-column support is checked per engine, including the PostgreSQL virtual -column restrictions. [PostgreSQL 18 generated columns](https://www.postgresql.org/docs/18/ddl-generated-columns.html). -The integration cases validate the actual installed SQLite and PostgreSQL behavior. - -## Validation - -The final native suite passes **872 tests in 10:41** with real SQLite and -PostgreSQL enabled. Real Chrome JS and Dart WASM each pass **20 checks**, including -new session/optional-selection/write guards. Static analysis passes. Exact logs, -runtime source digest and validation limits are in -[the final record](validation/api-audit.md). - -The first full attempt was stopped to complete the catalog-import correction. -It also exposed two existing decimal-average assertions that needed the shared -`QUERY.AGGREGATE` code, and a transient SQLite download TLS failure. Mutation -validation now uses the shared error code. Test consumers reuse the root's -content-addressed download cache through the normal hash-verifying sqlite3 hook. -The clean full rerun above supersedes that interrupted attempt. - -No Android/Apple runtime or performance capture was repeated. Earlier captures -remain historical evidence, not certification of this refactor. No push or -package publication is part of this review. diff --git a/research/benchmarks/authoring.json b/research/benchmarks/authoring.json deleted file mode 100644 index e52c3fd9..00000000 --- a/research/benchmarks/authoring.json +++ /dev/null @@ -1,1159 +0,0 @@ -{ - "recordedAt": "2026-09-15T08:45:16.612800Z", - "runtimeSourceCommit": "4ef499eece6fc09c9b41f3d3f07a3bbf3d4fae01", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "smoke": false, - "scope": "Controlled User/Post/Profile/Follow declaration-frontends experiment. All forms are resolved, normalized to the same Record schema, and sent through the unchanged production generator. Class/table inputs describe schema only; arbitrary constructors/methods and nominal runtime row objects are not implemented. No database execution or runtime performance claim. One timing observation per stage, warm SDK/pub/OS caches, offline dependency resolution excluded. Physical snapshots and generated Dart must be byte-identical for each variant. Record field rename is manual; class/table schema references use a clean LSP server. Generated-client references are independently checked and repaired after regeneration.", - "harnessSha256": { - "tool/compare_authoring.dart": "94d67e12617f4c31921ffde23c87f4db43ab8b416911052ba2f4bf726df00e38", - "tool/src/authoring.dart": "8c015925af9b5dd34eac1e07f68b68d07458463a880102d5f1d8180ceb7353b2", - "tool/src/authoring_fixture.dart": "26179f126778b3d1bfef7ff3e59f0b4b67b1adc89ff02381abddede0cf02188d", - "tool/src/build_fixture.dart": "03a5d875d608fd25245b5da268d66ce4c0913ed288ac05270ae9522a68bb0ea9", - "tool/src/editor_server.dart": "00e3cf5e7675166979cdb03a9d333754ee7aeb2874d6940d5d5ab6cff43a9ff8" - }, - "outputs": { - "base": { - "schema": "import 'package:orm/schema.dart';\ntypedef User = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"email\") String email,\n@ColumnName(\"nickname\") String? nickname,\n});\ntypedef Post = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"author_id\") int authorId,\n@ColumnName(\"title\") String title,\n@Default.sql('false') @ColumnName(\"published\") bool published,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\ntypedef Profile = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"user_id\") int userId,\n@ColumnName(\"bio\") String? bio,\n});\ntypedef Follow = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"follower_id\") int followerId,\n@ColumnName(\"followed_id\") int followedId,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "clientSha256": "f4b306b028261cb1c88d2c18d5e4d1dee57e65e9e1cc5561aab52e56d5c27c65", - "snapshot": { - "format": 1, - "tables": [ - { - "name": "users", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "email", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "nickname", - "type": "text", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [ - [ - "tenant_id", - "id" - ], - [ - "tenant_id", - "email" - ] - ], - "indexes": [], - "foreignKeys": [] - }, - { - "name": "posts", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "author_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "title", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "published", - "type": "boolean", - "nullable": false, - "generated": false, - "default": "false" - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [], - "indexes": [ - { - "name": "author_timeline", - "columns": [ - "tenant_id", - "author_id", - "created_at", - "id" - ], - "unique": false - } - ], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "author_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "RESTRICT" - } - ] - }, - { - "name": "profiles", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "user_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "bio", - "type": "text", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "user_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "user_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ] - }, - { - "name": "follows", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "follower_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "followed_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "follower_id", - "followed_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "follower_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - }, - { - "columns": [ - "tenant_id", - "followed_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ], - "checks": [ - { - "name": "distinct_users", - "sqlite": "follower_id <> followed_id", - "postgres": "follower_id <> followed_id" - } - ] - } - ] - } - }, - "field_type": { - "schema": "import 'package:orm/schema.dart';\ntypedef User = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"email\") String email,\n@ColumnName(\"nickname\") String? nickname,\n});\ntypedef Post = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"author_id\") int authorId,\n@ColumnName(\"title\") String title,\n@Default.sql('false') @ColumnName(\"published\") bool published,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\ntypedef Profile = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"user_id\") int userId,\n@ColumnName(\"bio\") int? bio,\n});\ntypedef Follow = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"follower_id\") int followerId,\n@ColumnName(\"followed_id\") int followedId,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "clientSha256": "9e71d404b664a5a686f3cd460d0ab41c9cc6fe9332412e7f896f65241f914152", - "snapshot": { - "format": 1, - "tables": [ - { - "name": "users", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "email", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "nickname", - "type": "text", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [ - [ - "tenant_id", - "id" - ], - [ - "tenant_id", - "email" - ] - ], - "indexes": [], - "foreignKeys": [] - }, - { - "name": "posts", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "author_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "title", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "published", - "type": "boolean", - "nullable": false, - "generated": false, - "default": "false" - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [], - "indexes": [ - { - "name": "author_timeline", - "columns": [ - "tenant_id", - "author_id", - "created_at", - "id" - ], - "unique": false - } - ], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "author_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "RESTRICT" - } - ] - }, - { - "name": "profiles", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "user_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "bio", - "type": "integer", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "user_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "user_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ] - }, - { - "name": "follows", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "follower_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "followed_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "follower_id", - "followed_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "follower_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - }, - { - "columns": [ - "tenant_id", - "followed_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ], - "checks": [ - { - "name": "distinct_users", - "sqlite": "follower_id <> followed_id", - "postgres": "follower_id <> followed_id" - } - ] - } - ] - } - }, - "rename": { - "schema": "import 'package:orm/schema.dart';\ntypedef User = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"email\") String contactEmail,\n@ColumnName(\"nickname\") String? nickname,\n});\ntypedef Post = ({\n@Id.generated() @ColumnName(\"id\") int id,\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"author_id\") int authorId,\n@ColumnName(\"title\") String title,\n@Default.sql('false') @ColumnName(\"published\") bool published,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\ntypedef Profile = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"user_id\") int userId,\n@ColumnName(\"bio\") String? bio,\n});\ntypedef Follow = ({\n@ColumnName(\"tenant_id\") int tenantId,\n@ColumnName(\"follower_id\") int followerId,\n@ColumnName(\"followed_id\") int followedId,\n@ColumnName(\"created_at\") DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.contactEmail));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "clientSha256": "421b7bf6338dd8e2116e3acaa1c0b887f1ee8c077e79e0009a13d4734c6c9adc", - "snapshot": { - "format": 1, - "tables": [ - { - "name": "users", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "email", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "nickname", - "type": "text", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [ - [ - "tenant_id", - "id" - ], - [ - "tenant_id", - "email" - ] - ], - "indexes": [], - "foreignKeys": [] - }, - { - "name": "posts", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false, - "generated": true - }, - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "author_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "title", - "type": "text", - "nullable": false, - "generated": false - }, - { - "name": "published", - "type": "boolean", - "nullable": false, - "generated": false, - "default": "false" - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "id" - ], - "uniqueKeys": [], - "indexes": [ - { - "name": "author_timeline", - "columns": [ - "tenant_id", - "author_id", - "created_at", - "id" - ], - "unique": false - } - ], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "author_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "RESTRICT" - } - ] - }, - { - "name": "profiles", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "user_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "bio", - "type": "text", - "nullable": true, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "user_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "user_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ] - }, - { - "name": "follows", - "columns": [ - { - "name": "tenant_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "follower_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "followed_id", - "type": "integer", - "nullable": false, - "generated": false - }, - { - "name": "created_at", - "type": "instant", - "nullable": false, - "generated": false - } - ], - "primaryKey": [ - "tenant_id", - "follower_id", - "followed_id" - ], - "uniqueKeys": [], - "indexes": [], - "foreignKeys": [ - { - "columns": [ - "tenant_id", - "follower_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - }, - { - "columns": [ - "tenant_id", - "followed_id" - ], - "target": "users", - "targetColumns": [ - "tenant_id", - "id" - ], - "onDelete": "CASCADE" - } - ], - "checks": [ - { - "name": "distinct_users", - "sqlite": "follower_id <> followed_id", - "postgres": "follower_id <> followed_id" - } - ] - } - ] - } - } - }, - "results": [ - { - "form": "record", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\ntypedef User = ({\n @Id.generated() int id,\n int tenantId,\n @ColumnName('email') String email,\n String? nickname,\n});\ntypedef Post = ({\n @Id.generated() int id,\n int tenantId,\n int authorId,\n String title,\n @Default.sql('false') bool published,\n DateTime createdAt,\n});\ntypedef Profile = ({int tenantId, int userId, String? bio});\ntypedef Follow = ({\n int tenantId,\n int followerId,\n int followedId,\n DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "base": { - "sourceBytes": 1843, - "sourceLines": 62, - "modelDeclarationBytes": 487, - "modelDeclarationLines": 24, - "normalizeMicros": 1401839, - "generationMicros": 510759, - "schemaSha256": "cd7afdcdbf23cf25963555e46af817e1be7914e08bb0c96033290976c2dbe7c9", - "clientSha256": "f4b306b028261cb1c88d2c18d5e4d1dee57e65e9e1cc5561aab52e56d5c27c65", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85" - }, - "initialAnalyzeMs": 467, - "fieldTypeEdit": { - "sourceBytes": 1840, - "sourceLines": 62, - "modelDeclarationBytes": 484, - "modelDeclarationLines": 24, - "normalizeMicros": 345208, - "generationMicros": 352162, - "schemaSha256": "a9f4abceec75636c58c9e81edbdaea637aad35f9c201fa814d509e4f8ec043fa", - "clientSha256": "9e71d404b664a5a686f3cd460d0ab41c9cc6fe9332412e7f896f65241f914152", - "snapshotSha256": "4b410fc14cf017b777c36062aa71b38bd6620e6869bd3a1b22397775cb0a7866", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\ntypedef User = ({\n @Id.generated() int id,\n int tenantId,\n @ColumnName('email') String email,\n String? nickname,\n});\ntypedef Post = ({\n @Id.generated() int id,\n int tenantId,\n int authorId,\n String title,\n @Default.sql('false') bool published,\n DateTime createdAt,\n});\ntypedef Profile = ({int tenantId, int userId, int? bio});\ntypedef Follow = ({\n int tenantId,\n int followerId,\n int followedId,\n DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({int? bio, String email, List following, List posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ] - }, - "fieldRename": { - "sourceBytes": 1857, - "sourceLines": 62, - "modelDeclarationBytes": 494, - "modelDeclarationLines": 24, - "normalizeMicros": 315184, - "generationMicros": 282625, - "schemaSha256": "6efc4a760f465a87b05ada8a66d38a9262c21730ae586d6caaaa24418abf1943", - "clientSha256": "421b7bf6338dd8e2116e3acaa1c0b887f1ee8c077e79e0009a13d4734c6c9adc", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\ntypedef User = ({\n @Id.generated() int id,\n int tenantId,\n @ColumnName('email') String contactEmail,\n String? nickname,\n});\ntypedef Post = ({\n @Id.generated() int id,\n int tenantId,\n int authorId,\n String title,\n @Default.sql('false') bool published,\n DateTime createdAt,\n});\ntypedef Profile = ({int tenantId, int userId, String? bio});\ntypedef Follow = ({\n int tenantId,\n int followerId,\n int followedId,\n DateTime createdAt,\n});\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.contactEmail));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "method": "manual", - "lspMicros": 2358, - "prepare": null, - "edit": null, - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|MISSING_REQUIRED_ARGUMENT|/privatefixture/lib/client.dart|5|31|6|The named parameter 'contactEmail' is required, but there's no corresponding argument.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_NAMED_PARAMETER|/privatefixture/lib/client.dart|5|51|5|The named parameter 'email' isn't defined.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|6|29|5|The getter 'email' isn't defined for the type 'User'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|9|7|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|12|58|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({dynamic bio, dynamic email, dynamic following, dynamic posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ], - "physicalSnapshotUnchanged": true - }, - "errors": [ - { - "case": "unknown_field", - "phase": "dart", - "code": "undefined_getter", - "message": "The getter 'absent' isn't defined for the type 'User'.", - "offset": 793, - "length": 6, - "line": 30, - "column": 54, - "sourceLine": "final userEmail = users.unique((u) => (u.tenantId, u.absent));" - }, - { - "case": "computed_selector", - "phase": "generator", - "code": "computed_selector", - "message": "Key selectors accept direct fields or positional records of fields only. (offset 2217)", - "offset": 1879, - "length": 1, - "line": 64, - "column": 36, - "sourceLine": "final invalid = users.index((u) => 0);" - }, - { - "case": "duplicate_key_field", - "phase": "generator", - "code": "duplicate_key_field", - "message": "A key cannot repeat a field. (offset 2224)", - "offset": 1886, - "length": 4, - "line": 64, - "column": 43, - "sourceLine": "final invalid = users.index((u) => (u.id, u.id));" - }, - { - "case": "non_unique_target", - "phase": "generator", - "code": "non_unique_target", - "message": "Foreign key target must be a primary or unique key in the same column order. (offset 1267)", - "offset": 928, - "length": 5, - "line": 34, - "column": 16, - "sourceLine": "final author = posts" - } - ], - "finalAnalyzeMs": 465 - }, - { - "form": "primary", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\nfinal class User(\n @Id.generated() final int id,\n final int tenantId,\n @ColumnName('email') final String email,\n final String? nickname,\n);\nfinal class Post(\n @Id.generated() final int id,\n final int tenantId,\n final int authorId,\n final String title,\n @Default.sql('false') final bool published,\n final DateTime createdAt,\n);\nfinal class Profile(final int tenantId, final int userId, final String? bio);\nfinal class Follow(\n final int tenantId,\n final int followerId,\n final int followedId,\n final DateTime createdAt,\n);\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "base": { - "sourceBytes": 1941, - "sourceLines": 62, - "modelDeclarationBytes": 585, - "modelDeclarationLines": 24, - "normalizeMicros": 240968, - "generationMicros": 256922, - "schemaSha256": "cd7afdcdbf23cf25963555e46af817e1be7914e08bb0c96033290976c2dbe7c9", - "clientSha256": "f4b306b028261cb1c88d2c18d5e4d1dee57e65e9e1cc5561aab52e56d5c27c65", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85" - }, - "initialAnalyzeMs": 465, - "fieldTypeEdit": { - "sourceBytes": 1938, - "sourceLines": 62, - "modelDeclarationBytes": 582, - "modelDeclarationLines": 24, - "normalizeMicros": 234572, - "generationMicros": 248713, - "schemaSha256": "a9f4abceec75636c58c9e81edbdaea637aad35f9c201fa814d509e4f8ec043fa", - "clientSha256": "9e71d404b664a5a686f3cd460d0ab41c9cc6fe9332412e7f896f65241f914152", - "snapshotSha256": "4b410fc14cf017b777c36062aa71b38bd6620e6869bd3a1b22397775cb0a7866", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\nfinal class User(\n @Id.generated() final int id,\n final int tenantId,\n @ColumnName('email') final String email,\n final String? nickname,\n);\nfinal class Post(\n @Id.generated() final int id,\n final int tenantId,\n final int authorId,\n final String title,\n @Default.sql('false') final bool published,\n final DateTime createdAt,\n);\nfinal class Profile(final int tenantId, final int userId, final int? bio);\nfinal class Follow(\n final int tenantId,\n final int followerId,\n final int followedId,\n final DateTime createdAt,\n);\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({int? bio, String email, List following, List posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ] - }, - "fieldRename": { - "sourceBytes": 1955, - "sourceLines": 62, - "modelDeclarationBytes": 592, - "modelDeclarationLines": 24, - "normalizeMicros": 225761, - "generationMicros": 229178, - "schemaSha256": "6efc4a760f465a87b05ada8a66d38a9262c21730ae586d6caaaa24418abf1943", - "clientSha256": "421b7bf6338dd8e2116e3acaa1c0b887f1ee8c077e79e0009a13d4734c6c9adc", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\n\nfinal class User(\n @Id.generated() final int id,\n final int tenantId,\n @ColumnName('email') final String contactEmail,\n final String? nickname,\n);\nfinal class Post(\n @Id.generated() final int id,\n final int tenantId,\n final int authorId,\n final String title,\n @Default.sql('false') final bool published,\n final DateTime createdAt,\n);\nfinal class Profile(final int tenantId, final int userId, final String? bio);\nfinal class Follow(\n final int tenantId,\n final int followerId,\n final int followedId,\n final DateTime createdAt,\n);\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.contactEmail));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "method": "lsp", - "lspMicros": 321910, - "prepare": { - "placeholder": "email", - "range": { - "end": { - "character": 41, - "line": 6 - }, - "start": { - "character": 36, - "line": 6 - } - } - }, - "edit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "contactEmail", - "range": { - "end": { - "character": 41, - "line": 6 - }, - "start": { - "character": 36, - "line": 6 - } - } - }, - { - "newText": "contactEmail", - "range": { - "end": { - "character": 58, - "line": 29 - }, - "start": { - "character": 53, - "line": 29 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/author.dart", - "version": 1 - } - } - ] - }, - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|MISSING_REQUIRED_ARGUMENT|/privatefixture/lib/client.dart|5|31|6|The named parameter 'contactEmail' is required, but there's no corresponding argument.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_NAMED_PARAMETER|/privatefixture/lib/client.dart|5|51|5|The named parameter 'email' isn't defined.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|6|29|5|The getter 'email' isn't defined for the type 'User'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|9|7|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|12|58|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({dynamic bio, dynamic email, dynamic following, dynamic posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ], - "physicalSnapshotUnchanged": true - }, - "errors": [ - { - "case": "unknown_field", - "phase": "dart", - "code": "undefined_getter", - "message": "The getter 'absent' isn't defined for the type 'User'.", - "offset": 891, - "length": 6, - "line": 30, - "column": 54, - "sourceLine": "final userEmail = users.unique((u) => (u.tenantId, u.absent));" - }, - { - "case": "computed_selector", - "phase": "generator", - "code": "computed_selector", - "message": "Key selectors accept direct fields or positional records of fields only. (offset 2217)", - "offset": 1977, - "length": 1, - "line": 64, - "column": 36, - "sourceLine": "final invalid = users.index((u) => 0);" - }, - { - "case": "duplicate_key_field", - "phase": "generator", - "code": "duplicate_key_field", - "message": "A key cannot repeat a field. (offset 2224)", - "offset": 1984, - "length": 4, - "line": 64, - "column": 43, - "sourceLine": "final invalid = users.index((u) => (u.id, u.id));" - }, - { - "case": "non_unique_target", - "phase": "generator", - "code": "non_unique_target", - "message": "Foreign key target must be a primary or unique key in the same column order. (offset 1267)", - "offset": 1026, - "length": 5, - "line": 34, - "column": 16, - "sourceLine": "final author = posts" - }, - { - "case": "constructor_logic", - "phase": "authoring", - "code": "unsupported_class", - "message": "Use an unmodified primary constructor with declaring fields only.", - "offset": 49, - "length": 193, - "line": 4, - "column": 1, - "sourceLine": "final class User(" - } - ], - "finalAnalyzeMs": 464 - }, - { - "form": "table", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\nimport 'package:orm/orm.dart';\n\nfinal class User extends Fields {\n User(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final email = column(Column('email', Codecs.text));\n late final nickname = column(\n Column('nickname', Codecs.text.nullable(), nullable: true),\n );\n}\n\nfinal class Post extends Fields {\n Post(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final authorId = column(Column('author_id', Codecs.integer));\n late final title = column(Column('title', Codecs.text));\n @Default.sql('false')\n late final published = column(Column('published', Codecs.boolean));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal class Profile extends Fields {\n Profile(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final userId = column(Column('user_id', Codecs.integer));\n late final bio = column(\n Column('bio', Codecs.text.nullable(), nullable: true),\n );\n}\n\nfinal class Follow extends Fields {\n Follow(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final followerId = column(Column('follower_id', Codecs.integer));\n late final followedId = column(Column('followed_id', Codecs.integer));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "base": { - "sourceBytes": 2930, - "sourceLines": 82, - "modelDeclarationBytes": 1574, - "modelDeclarationLines": 43, - "normalizeMicros": 239366, - "generationMicros": 242599, - "schemaSha256": "cd7afdcdbf23cf25963555e46af817e1be7914e08bb0c96033290976c2dbe7c9", - "clientSha256": "f4b306b028261cb1c88d2c18d5e4d1dee57e65e9e1cc5561aab52e56d5c27c65", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85" - }, - "initialAnalyzeMs": 465, - "fieldTypeEdit": { - "sourceBytes": 2933, - "sourceLines": 82, - "modelDeclarationBytes": 1577, - "modelDeclarationLines": 43, - "normalizeMicros": 224609, - "generationMicros": 241511, - "schemaSha256": "a9f4abceec75636c58c9e81edbdaea637aad35f9c201fa814d509e4f8ec043fa", - "clientSha256": "9e71d404b664a5a686f3cd460d0ab41c9cc6fe9332412e7f896f65241f914152", - "snapshotSha256": "4b410fc14cf017b777c36062aa71b38bd6620e6869bd3a1b22397775cb0a7866", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\nimport 'package:orm/orm.dart';\n\nfinal class User extends Fields {\n User(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final email = column(Column('email', Codecs.text));\n late final nickname = column(\n Column('nickname', Codecs.text.nullable(), nullable: true),\n );\n}\n\nfinal class Post extends Fields {\n Post(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final authorId = column(Column('author_id', Codecs.integer));\n late final title = column(Column('title', Codecs.text));\n @Default.sql('false')\n late final published = column(Column('published', Codecs.boolean));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal class Profile extends Fields {\n Profile(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final userId = column(Column('user_id', Codecs.integer));\n late final bio = column(\n Column('bio', Codecs.integer.nullable(), nullable: true),\n );\n}\n\nfinal class Follow extends Fields {\n Follow(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final followerId = column(Column('follower_id', Codecs.integer));\n late final followedId = column(Column('followed_id', Codecs.integer));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.email));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({int? bio, String email, List following, List posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ] - }, - "fieldRename": { - "sourceBytes": 2944, - "sourceLines": 82, - "modelDeclarationBytes": 1581, - "modelDeclarationLines": 43, - "normalizeMicros": 224387, - "generationMicros": 237466, - "schemaSha256": "6efc4a760f465a87b05ada8a66d38a9262c21730ae586d6caaaa24418abf1943", - "clientSha256": "421b7bf6338dd8e2116e3acaa1c0b887f1ee8c077e79e0009a13d4734c6c9adc", - "snapshotSha256": "8fdeb6639ee4f6bc1358d356f305095de9462b6a14b8c4fdd2bde8ca626afa85", - "source": "// @dart=3.13\nimport 'package:orm/schema.dart';\nimport 'package:orm/orm.dart';\n\nfinal class User extends Fields {\n User(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final contactEmail = column(Column('email', Codecs.text));\n late final nickname = column(\n Column('nickname', Codecs.text.nullable(), nullable: true),\n );\n}\n\nfinal class Post extends Fields {\n Post(super.table);\n @Id.generated()\n late final id = column(Column('id', Codecs.integer));\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final authorId = column(Column('author_id', Codecs.integer));\n late final title = column(Column('title', Codecs.text));\n @Default.sql('false')\n late final published = column(Column('published', Codecs.boolean));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal class Profile extends Fields {\n Profile(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final userId = column(Column('user_id', Codecs.integer));\n late final bio = column(\n Column('bio', Codecs.text.nullable(), nullable: true),\n );\n}\n\nfinal class Follow extends Fields {\n Follow(super.table);\n late final tenantId = column(Column('tenant_id', Codecs.integer));\n late final followerId = column(Column('follower_id', Codecs.integer));\n late final followedId = column(Column('followed_id', Codecs.integer));\n late final createdAt = column(Column('created_at', Codecs.dateTime));\n}\n\nfinal users = entity(table: 'users');\nfinal posts = entity(table: 'posts');\nfinal profiles = entity(table: 'profiles');\nfinal follows = entity(table: 'follows');\nfinal userIdentity = users.unique((u) => (u.tenantId, u.id));\nfinal userEmail = users.unique((u) => (u.tenantId, u.contactEmail));\nfinal profileIdentity = profiles.primaryKey((p) => (p.tenantId, p.userId));\nfinal followIdentity = follows.primaryKey(\n (f) => (f.tenantId, f.followerId, f.followedId),\n);\nfinal author = posts\n .key((p) => (p.tenantId, p.authorId))\n .references(users.key((u) => (u.tenantId, u.id)), inverse: 'posts');\nfinal profileOwner = profiles\n .key((p) => (p.tenantId, p.userId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'profile',\n onDelete: .cascade,\n );\nfinal follower = follows\n .key((f) => (f.tenantId, f.followerId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'following',\n onDelete: .cascade,\n );\nfinal followed = follows\n .key((f) => (f.tenantId, f.followedId))\n .references(\n users.key((u) => (u.tenantId, u.id)),\n inverse: 'followers',\n onDelete: .cascade,\n );\nfinal authorTimeline = posts.index(\n (p) => (p.tenantId, p.authorId, p.createdAt, p.id),\n);\nfinal distinctUsers = follows.check('follower_id <> followed_id');\n", - "method": "lsp", - "lspMicros": 313547, - "prepare": { - "placeholder": "email", - "range": { - "end": { - "character": 18, - "line": 9 - }, - "start": { - "character": 13, - "line": 9 - } - } - }, - "edit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "contactEmail", - "range": { - "end": { - "character": 18, - "line": 9 - }, - "start": { - "character": 13, - "line": 9 - } - } - }, - { - "newText": "contactEmail", - "range": { - "end": { - "character": 58, - "line": 49 - }, - "start": { - "character": 53, - "line": 49 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/author.dart", - "version": 1 - } - } - ] - }, - "staleConsumerDiagnostics": [ - "ERROR|COMPILE_TIME_ERROR|MISSING_REQUIRED_ARGUMENT|/privatefixture/lib/client.dart|5|31|6|The named parameter 'contactEmail' is required, but there's no corresponding argument.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_NAMED_PARAMETER|/privatefixture/lib/client.dart|5|51|5|The named parameter 'email' isn't defined.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|6|29|5|The getter 'email' isn't defined for the type 'User'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|9|7|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|UNDEFINED_GETTER|/privatefixture/lib/client.dart|12|58|5|The getter 'email' isn't defined for the type 'UsersFields'.", - "ERROR|COMPILE_TIME_ERROR|INVALID_ASSIGNMENT|/privatefixture/lib/client.dart|14|97|5|A value of type 'List<({dynamic bio, dynamic email, dynamic following, dynamic posts})>' can't be assigned to a variable of type 'List<({String? bio, String email, List following, List posts})>'." - ], - "physicalSnapshotUnchanged": true - }, - "errors": [ - { - "case": "unknown_field", - "phase": "dart", - "code": "undefined_getter", - "message": "The getter 'absent' isn't defined for the type 'User'.", - "offset": 1880, - "length": 6, - "line": 50, - "column": 54, - "sourceLine": "final userEmail = users.unique((u) => (u.tenantId, u.absent));" - }, - { - "case": "computed_selector", - "phase": "generator", - "code": "computed_selector", - "message": "Key selectors accept direct fields or positional records of fields only. (offset 2217)", - "offset": 2966, - "length": 1, - "line": 84, - "column": 36, - "sourceLine": "final invalid = users.index((u) => 0);" - }, - { - "case": "duplicate_key_field", - "phase": "generator", - "code": "duplicate_key_field", - "message": "A key cannot repeat a field. (offset 2224)", - "offset": 2973, - "length": 4, - "line": 84, - "column": 43, - "sourceLine": "final invalid = users.index((u) => (u.id, u.id));" - }, - { - "case": "non_unique_target", - "phase": "generator", - "code": "non_unique_target", - "message": "Foreign key target must be a primary or unique key in the same column order. (offset 1267)", - "offset": 2015, - "length": 5, - "line": 54, - "column": 16, - "sourceLine": "final author = posts" - }, - { - "case": "custom_codec", - "phase": "authoring", - "code": "unsupported_codec", - "message": "Use the matching built-in codec and explicit SQL nullability; custom conversions cannot be discarded.", - "offset": 392, - "length": 46, - "line": 12, - "column": 24, - "sourceLine": " Column('nickname', Codecs.text.map((v) => v, (v) => v).nullable(), nullable: true)," - } - ], - "finalAnalyzeMs": 465 - } - ] -} diff --git a/research/benchmarks/decimal-average.json b/research/benchmarks/decimal-average.json deleted file mode 100644 index 7b58d58f..00000000 --- a/research/benchmarks/decimal-average.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "recordedAt": "2026-09-15T01:00:42.064966Z", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "warmupRuns": 1, - "measuredRuns": 3, - "scope": "Single-client end-to-end query fetch and Decimal decoding; 10000 integer-valued rows; not a concurrency or percentile benchmark.", - "results": [ - { - "backend": "sqlite", - "version": "3.51.0", - "case": "read", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 11.392, - 10.143, - 9.747 - ], - "sqlBytes": 70 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "average_half_even_scale_2", - "inputRows": 10000, - "rows": 1, - "elapsedMs": [ - 8.821, - 8.78, - 8.599 - ], - "sqlBytes": 125 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "window_average_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 28.88, - 29.125, - 30.176 - ], - "sqlBytes": 204 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "divide_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 32.28, - 30.836, - 30.717 - ], - "sqlBytes": 154 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "round_half_even_scale_minus_1", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 22.987, - 22.628, - 23.365 - ], - "sqlBytes": 126 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "window_divide_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 52.056, - 51.782, - 53.221 - ], - "sqlBytes": 280 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "read", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 25.067, - 24.24, - 25.092 - ], - "sqlBytes": 43 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "average_half_even_scale_2", - "inputRows": 10000, - "rows": 1, - "elapsedMs": [ - 2.751, - 2.666, - 2.739 - ], - "sqlBytes": 2282 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "window_average_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 46.994, - 47.254, - 49.207 - ], - "sqlBytes": 2637 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "divide_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 34.445, - 34.518, - 34.378 - ], - "sqlBytes": 1597 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "round_half_even_scale_minus_1", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 25.926, - 26.494, - 25.991 - ], - "sqlBytes": 1149 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "window_divide_half_even_scale_2", - "inputRows": 10000, - "rows": 10000, - "elapsedMs": [ - 38.94, - 38.582, - 38.361 - ], - "sqlBytes": 1773 - } - ] -} diff --git a/research/benchmarks/decimal-division.json b/research/benchmarks/decimal-division.json deleted file mode 100644 index 835ca903..00000000 --- a/research/benchmarks/decimal-division.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "recordedAt": "2026-09-15T00:38:41.193708Z", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "warmupRuns": 1, - "measuredRuns": 3, - "scope": "Single-client end-to-end query fetch and Decimal decoding; 10000 integer-valued rows; not a concurrency or percentile benchmark.", - "results": [ - { - "backend": "sqlite", - "version": "3.51.0", - "case": "read", - "rows": 10000, - "elapsedMs": [ - 11.068, - 10.129, - 9.57 - ], - "sqlBytes": 70 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "divide_half_even_scale_2", - "rows": 10000, - "elapsedMs": [ - 30.672, - 30.095, - 29.656 - ], - "sqlBytes": 154 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "round_half_even_scale_minus_1", - "rows": 10000, - "elapsedMs": [ - 21.219, - 22.125, - 21.644 - ], - "sqlBytes": 126 - }, - { - "backend": "sqlite", - "version": "3.51.0", - "case": "window_divide_half_even_scale_2", - "rows": 10000, - "elapsedMs": [ - 46.842, - 48.727, - 48.175 - ], - "sqlBytes": 280 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "read", - "rows": 10000, - "elapsedMs": [ - 21.922, - 23.661, - 22.724 - ], - "sqlBytes": 43 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "divide_half_even_scale_2", - "rows": 10000, - "elapsedMs": [ - 32.665, - 32.626, - 32.521 - ], - "sqlBytes": 1597 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "round_half_even_scale_minus_1", - "rows": 10000, - "elapsedMs": [ - 25.258, - 25.168, - 25.245 - ], - "sqlBytes": 1149 - }, - { - "backend": "postgres", - "version": "18.4 (Homebrew)", - "case": "window_divide_half_even_scale_2", - "rows": 10000, - "elapsedMs": [ - 36.849, - 36.591, - 37.124 - ], - "sqlBytes": 1773 - } - ] -} diff --git a/research/benchmarks/editor.json b/research/benchmarks/editor.json deleted file mode 100644 index d9f2610e..00000000 --- a/research/benchmarks/editor.json +++ /dev/null @@ -1,2801 +0,0 @@ -{ - "recordedAt": "2026-09-15T08:19:35.236903Z", - "runtimeSourceCommit": "7eaab3bf83a4cb183132ff1ba669e7fb4c4b5c25", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "cpu": "Apple M3 Max", - "smoke": false, - "samplesPerWarmProbe": 20, - "harnessSha256": { - "tool/benchmark_editor.dart": "88bbe4fe83ac6579e778e86f5b1c3bcb0d04c5db1e4046de0a88952b0180130a", - "tool/src/editor_server.dart": "00e3cf5e7675166979cdb03a9d333754ee7aeb2874d6940d5d5ab6cff43a9ff8", - "tool/src/build_fixture.dart": "03a5d875d608fd25245b5da268d66ce4c0913ed288ac05270ae9522a68bb0ea9" - }, - "scope": "Direct stdio LSP client against the installed Dart Analysis Server, isolated consumer packages and real generated APIs; no database. Includes client transport/JSON costs, excludes GUI/plugin rendering and dependency download. Each scale uses one fresh server for completion/diagnostics and another for symbol renames. Startup and initial project analysis are measured separately from completion. First completion follows a fresh diagnostic for the incomplete source and is not a cold OS/cache measurement. Warm probes use unchanged documents after two unrecorded warmups. Diagnostic probes serialize changes and await a fresh notification; this SDK omits document versions. Final CLI analysis verifies the edited sources. Symbol-form probes compare language rename behavior only, not three ORM authoring frontends or identical generated APIs from each form.", - "limits": [ - "During harness development, primary-constructor field rename after the error/repair sequence stalled for 60 seconds in the same server. A separate clean server completes the symbol probes; this report does not establish reliable mixed-session rename after diagnostic recovery." - ], - "results": [ - { - "models": 10, - "sourceBytes": 1608, - "generatedBytes": 22953, - "initializeMicros": 34025, - "projectReadyMicros": 310640, - "completions": [ - { - "probe": "table_entries", - "analysisReadyMicros": 6287, - "firstRequestMicros": 2650, - "warm": { - "samplesMicros": [ - 1047, - 1117, - 1035, - 945, - 927, - 821, - 829, - 2734, - 884, - 838, - 809, - 772, - 772, - 809, - 776, - 765, - 720, - 709, - 716, - 705 - ], - "p50Micros": 809, - "p95Micros": 1117 - }, - "response": { - "isIncomplete": false, - "itemCount": 31, - "items": [ - { - "label": "rows0", - "kind": 10, - "detail": "Rows0TableSet", - "labelDetails": { - "detail": " Rows0TableSet" - }, - "textEdit": { - "newText": "rows0", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - }, - { - "label": "rows9", - "kind": 10, - "detail": "Rows9TableSet", - "labelDetails": { - "detail": " Rows9TableSet" - }, - "textEdit": { - "newText": "rows9", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "rows0", - "rows9" - ], - "forbiddenLabels": [] - }, - { - "probe": "schema_record_fields", - "analysisReadyMicros": 7617, - "firstRequestMicros": 777, - "warm": { - "samplesMicros": [ - 545, - 492, - 439, - 461, - 438, - 443, - 424, - 419, - 458, - 463, - 452, - 511, - 442, - 462, - 408, - 439, - 426, - 409, - 403, - 398 - ], - "p50Micros": 439, - "p95Micros": 511 - }, - "response": { - "isIncomplete": false, - "itemCount": 8, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "score", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "status", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "query_fields", - "analysisReadyMicros": 10510, - "firstRequestMicros": 1792, - "warm": { - "samplesMicros": [ - 780, - 749, - 1701, - 604, - 541, - 617, - 533, - 564, - 458, - 439, - 480, - 484, - 485, - 457, - 464, - 446, - 435, - 442, - 453, - 1126 - ], - "p50Micros": 484, - "p95Micros": 1126 - }, - "response": { - "isIncomplete": false, - "itemCount": 11, - "items": [ - { - "label": "id", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "score", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "status", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "create_arguments", - "analysisReadyMicros": 4884, - "firstRequestMicros": 455, - "warm": { - "samplesMicros": [ - 307, - 262, - 293, - 272, - 233, - 225, - 246, - 216, - 247, - 247, - 227, - 219, - 259, - 243, - 218, - 235, - 266, - 238, - 213, - 203 - ], - "p50Micros": 238, - "p95Micros": 293 - }, - "response": { - "isIncomplete": false, - "itemCount": 5, - "items": [ - { - "label": "title:", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - }, - { - "label": "status:", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "title:", - "status:" - ], - "forbiddenLabels": [] - }, - { - "probe": "projected_record_fields", - "analysisReadyMicros": 5305, - "firstRequestMicros": 640, - "warm": { - "samplesMicros": [ - 518, - 539, - 514, - 505, - 553, - 515, - 1620, - 590, - 491, - 500, - 517, - 504, - 512, - 521, - 505, - 541, - 547, - 537, - 520, - 535 - ], - "p50Micros": 518, - "p95Micros": 590 - }, - "response": { - "isIncomplete": false, - "itemCount": 6, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title" - ], - "forbiddenLabels": [ - "score", - "status" - ] - } - ], - "diagnostics": [ - { - "probe": "create_type", - "micros": 5058, - "version": 5, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 77 - }, - "end": { - "line": 3, - "character": 80 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'int' can't be assigned to the parameter type 'String'. ", - "range": { - "end": { - "character": 80, - "line": 3 - }, - "start": { - "character": 77, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - } - ] - }, - { - "probe": "predicate_type", - "micros": 5817, - "version": 6, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 81 - }, - "end": { - "line": 3, - "character": 88 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'String' can't be assigned to the parameter type 'int'. ", - "range": { - "end": { - "character": 88, - "line": 3 - }, - "start": { - "character": 81, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "selected_result", - "micros": 4517, - "version": 7, - "expectedCode": "invalid_assignment", - "expectedRange": { - "start": { - "line": 3, - "character": 124 - }, - "end": { - "line": 3, - "character": 134 - } - }, - "diagnostics": [ - { - "code": "invalid_assignment", - "message": "A value of type 'String' can't be assigned to a variable of type 'int'.\nTry changing the type of the variable, or casting the right-hand type to 'int'.", - "range": { - "end": { - "character": 134, - "line": 3 - }, - "start": { - "character": 124, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "missing_selected_field", - "micros": 56852, - "version": 8, - "expectedCode": "undefined_getter", - "expectedRange": { - "start": { - "line": 3, - "character": 169 - }, - "end": { - "line": 3, - "character": 174 - } - }, - "diagnostics": [ - { - "code": "undefined_getter", - "message": "The getter 'score' isn't defined for the type '({int id, String title})'.\nTry importing the library that defines 'score', correcting the name to the name of an existing getter, or defining a getter or field named 'score'.", - "range": { - "end": { - "character": 174, - "line": 3 - }, - "start": { - "character": 169, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - } - ], - "repairMicros": 10153, - "renameServerStartupMicros": 270113, - "symbolRenames": [ - { - "probe": "record_field", - "status": "unavailable", - "micros": 10803, - "prepare": null, - "error": null, - "workspaceEdit": null, - "applied": null - }, - { - "probe": "primary_constructor_field", - "status": "applied", - "micros": 639112, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 46, - "line": 12 - }, - "start": { - "character": 42, - "line": 12 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 1 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final name = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.name;\n" - } - } - }, - { - "probe": "table_field", - "status": "applied", - "micros": 33684, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 54, - "line": 13 - }, - "start": { - "character": 50, - "line": 13 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 2 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - }, - { - "probe": "record_alias", - "status": "applied", - "micros": 4803, - "prepare": { - "placeholder": "RecordUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 29, - "line": 9 - }, - "start": { - "character": 19, - "line": 9 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 10, - "line": 11 - }, - "start": { - "character": 0, - "line": 11 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 3 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 3, - "result": "import 'package:orm/orm.dart';\ntypedef RenamedUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RenamedUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRenamedUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - } - ], - "ormRule": { - "dartAnalysisMilliseconds": 95, - "dartAnalysisOutput": "Analyzing schema.dart...\nNo issues found!\n\n", - "generatorError": "Key selectors accept direct fields or positional records of fields only. (offset 1644)" - }, - "finalAnalyzeMilliseconds": 462 - }, - { - "models": 100, - "sourceBytes": 15918, - "generatedBytes": 230943, - "initializeMicros": 31152, - "projectReadyMicros": 487323, - "completions": [ - { - "probe": "table_entries", - "analysisReadyMicros": 9256, - "firstRequestMicros": 2120, - "warm": { - "samplesMicros": [ - 1545, - 1497, - 1498, - 2812, - 1473, - 1452, - 1465, - 1438, - 1427, - 1551, - 1566, - 1441, - 1462, - 1444, - 1453, - 3166, - 2472, - 2217, - 2212, - 2330 - ], - "p50Micros": 1497, - "p95Micros": 2812 - }, - "response": { - "isIncomplete": false, - "itemCount": 121, - "items": [ - { - "label": "rows0", - "kind": 10, - "detail": "Rows0TableSet", - "labelDetails": { - "detail": " Rows0TableSet" - }, - "textEdit": { - "newText": "rows0", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - }, - { - "label": "rows99", - "kind": 10, - "detail": "Rows99TableSet", - "labelDetails": { - "detail": " Rows99TableSet" - }, - "textEdit": { - "newText": "rows99", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "rows0", - "rows99" - ], - "forbiddenLabels": [] - }, - { - "probe": "schema_record_fields", - "analysisReadyMicros": 26581, - "firstRequestMicros": 663, - "warm": { - "samplesMicros": [ - 458, - 414, - 413, - 473, - 447, - 422, - 414, - 418, - 424, - 449, - 471, - 421, - 428, - 435, - 2102, - 492, - 418, - 410, - 453, - 442 - ], - "p50Micros": 428, - "p95Micros": 492 - }, - "response": { - "isIncomplete": false, - "itemCount": 8, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 39, - "line": 205 - }, - "start": { - "character": 39, - "line": 205 - } - } - } - }, - { - "label": "score", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 39, - "line": 205 - }, - "start": { - "character": 39, - "line": 205 - } - } - } - }, - { - "label": "status", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 39, - "line": 205 - }, - "start": { - "character": 39, - "line": 205 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 39, - "line": 205 - }, - "start": { - "character": 39, - "line": 205 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "query_fields", - "analysisReadyMicros": 17499, - "firstRequestMicros": 4209, - "warm": { - "samplesMicros": [ - 559, - 510, - 541, - 493, - 473, - 468, - 536, - 504, - 470, - 485, - 476, - 464, - 460, - 447, - 1978, - 513, - 484, - 468, - 486, - 469 - ], - "p50Micros": 484, - "p95Micros": 559 - }, - "response": { - "isIncomplete": false, - "itemCount": 11, - "items": [ - { - "label": "id", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "score", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "status", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "create_arguments", - "analysisReadyMicros": 6619, - "firstRequestMicros": 444, - "warm": { - "samplesMicros": [ - 255, - 289, - 263, - 266, - 243, - 232, - 235, - 221, - 227, - 205, - 217, - 217, - 208, - 250, - 257, - 252, - 389, - 315, - 271, - 258 - ], - "p50Micros": 250, - "p95Micros": 315 - }, - "response": { - "isIncomplete": false, - "itemCount": 5, - "items": [ - { - "label": "title:", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - }, - { - "label": "status:", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "title:", - "status:" - ], - "forbiddenLabels": [] - }, - { - "probe": "projected_record_fields", - "analysisReadyMicros": 7882, - "firstRequestMicros": 671, - "warm": { - "samplesMicros": [ - 600, - 570, - 584, - 582, - 570, - 568, - 1273, - 596, - 564, - 583, - 565, - 567, - 558, - 564, - 561, - 556, - 568, - 554, - 583, - 532 - ], - "p50Micros": 568, - "p95Micros": 600 - }, - "response": { - "isIncomplete": false, - "itemCount": 6, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title" - ], - "forbiddenLabels": [ - "score", - "status" - ] - } - ], - "diagnostics": [ - { - "probe": "create_type", - "micros": 9978, - "version": 5, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 77 - }, - "end": { - "line": 3, - "character": 80 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'int' can't be assigned to the parameter type 'String'. ", - "range": { - "end": { - "character": 80, - "line": 3 - }, - "start": { - "character": 77, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - } - ] - }, - { - "probe": "predicate_type", - "micros": 6571, - "version": 6, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 81 - }, - "end": { - "line": 3, - "character": 88 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'String' can't be assigned to the parameter type 'int'. ", - "range": { - "end": { - "character": 88, - "line": 3 - }, - "start": { - "character": 81, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "selected_result", - "micros": 7602, - "version": 7, - "expectedCode": "invalid_assignment", - "expectedRange": { - "start": { - "line": 3, - "character": 124 - }, - "end": { - "line": 3, - "character": 134 - } - }, - "diagnostics": [ - { - "code": "invalid_assignment", - "message": "A value of type 'String' can't be assigned to a variable of type 'int'.\nTry changing the type of the variable, or casting the right-hand type to 'int'.", - "range": { - "end": { - "character": 134, - "line": 3 - }, - "start": { - "character": 124, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "missing_selected_field", - "micros": 9357, - "version": 8, - "expectedCode": "undefined_getter", - "expectedRange": { - "start": { - "line": 3, - "character": 169 - }, - "end": { - "line": 3, - "character": 174 - } - }, - "diagnostics": [ - { - "code": "undefined_getter", - "message": "The getter 'score' isn't defined for the type '({int id, String title})'.\nTry importing the library that defines 'score', correcting the name to the name of an existing getter, or defining a getter or field named 'score'.", - "range": { - "end": { - "character": 174, - "line": 3 - }, - "start": { - "character": 169, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - } - ], - "repairMicros": 9399, - "renameServerStartupMicros": 268255, - "symbolRenames": [ - { - "probe": "record_field", - "status": "unavailable", - "micros": 14962, - "prepare": null, - "error": null, - "workspaceEdit": null, - "applied": null - }, - { - "probe": "primary_constructor_field", - "status": "applied", - "micros": 590193, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 46, - "line": 12 - }, - "start": { - "character": 42, - "line": 12 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 1 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final name = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.name;\n" - } - } - }, - { - "probe": "table_field", - "status": "applied", - "micros": 41126, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 54, - "line": 13 - }, - "start": { - "character": 50, - "line": 13 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 2 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - }, - { - "probe": "record_alias", - "status": "applied", - "micros": 4995, - "prepare": { - "placeholder": "RecordUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 29, - "line": 9 - }, - "start": { - "character": 19, - "line": 9 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 10, - "line": 11 - }, - "start": { - "character": 0, - "line": 11 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 3 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 3, - "result": "import 'package:orm/orm.dart';\ntypedef RenamedUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RenamedUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRenamedUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - } - ], - "ormRule": { - "dartAnalysisMilliseconds": 104, - "dartAnalysisOutput": "Analyzing schema.dart...\nNo issues found!\n\n", - "generatorError": "Key selectors accept direct fields or positional records of fields only. (offset 15954)" - }, - "finalAnalyzeMilliseconds": 512 - }, - { - "models": 1000, - "sourceBytes": 162618, - "generatedBytes": 2343243, - "initializeMicros": 32252, - "projectReadyMicros": 3404717, - "completions": [ - { - "probe": "table_entries", - "analysisReadyMicros": 42685, - "firstRequestMicros": 10672, - "warm": { - "samplesMicros": [ - 11593, - 9543, - 12990, - 17094, - 11936, - 10767, - 9421, - 9570, - 15206, - 18331, - 10945, - 9720, - 9613, - 11572, - 13042, - 10145, - 12720, - 9240, - 12030, - 14791 - ], - "p50Micros": 11572, - "p95Micros": 17094 - }, - "response": { - "isIncomplete": false, - "itemCount": 1021, - "items": [ - { - "label": "rows0", - "kind": 10, - "detail": "Rows0TableSet", - "labelDetails": { - "detail": " Rows0TableSet" - }, - "textEdit": { - "newText": "rows0", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - }, - { - "label": "rows999", - "kind": 10, - "detail": "Rows999TableSet", - "labelDetails": { - "detail": " Rows999TableSet" - }, - "textEdit": { - "newText": "rows999", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "rows0", - "rows999" - ], - "forbiddenLabels": [] - }, - { - "probe": "schema_record_fields", - "analysisReadyMicros": 165844, - "firstRequestMicros": 3371, - "warm": { - "samplesMicros": [ - 722, - 753, - 723, - 691, - 744, - 773, - 788, - 704, - 730, - 692, - 691, - 6504, - 730, - 663, - 653, - 698, - 705, - 650, - 705, - 698 - ], - "p50Micros": 705, - "p95Micros": 788 - }, - "response": { - "isIncomplete": false, - "itemCount": 8, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 39, - "line": 2005 - }, - "start": { - "character": 39, - "line": 2005 - } - } - } - }, - { - "label": "score", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 39, - "line": 2005 - }, - "start": { - "character": 39, - "line": 2005 - } - } - } - }, - { - "label": "status", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 39, - "line": 2005 - }, - "start": { - "character": 39, - "line": 2005 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 39, - "line": 2005 - }, - "start": { - "character": 39, - "line": 2005 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "query_fields", - "analysisReadyMicros": 128488, - "firstRequestMicros": 4109, - "warm": { - "samplesMicros": [ - 850, - 797, - 787, - 835, - 766, - 822, - 741, - 1606, - 767, - 797, - 764, - 765, - 829, - 800, - 746, - 780, - 807, - 755, - 755, - 753 - ], - "p50Micros": 780, - "p95Micros": 850 - }, - "response": { - "isIncomplete": false, - "itemCount": 11, - "items": [ - { - "label": "id", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "score", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "status", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "create_arguments", - "analysisReadyMicros": 38272, - "firstRequestMicros": 1763, - "warm": { - "samplesMicros": [ - 249, - 237, - 215, - 238, - 217, - 213, - 196, - 307, - 224, - 217, - 387, - 277, - 233, - 214, - 231, - 230, - 223, - 218, - 232, - 226 - ], - "p50Micros": 226, - "p95Micros": 307 - }, - "response": { - "isIncomplete": false, - "itemCount": 5, - "items": [ - { - "label": "title:", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - }, - { - "label": "status:", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "title:", - "status:" - ], - "forbiddenLabels": [] - }, - { - "probe": "projected_record_fields", - "analysisReadyMicros": 50153, - "firstRequestMicros": 3097, - "warm": { - "samplesMicros": [ - 2649, - 2863, - 1972, - 1514, - 4880, - 1330, - 1294, - 1336, - 1330, - 1331, - 1337, - 1291, - 1142, - 1676, - 1098, - 1097, - 1105, - 1107, - 1100, - 1129 - ], - "p50Micros": 1330, - "p95Micros": 2863 - }, - "response": { - "isIncomplete": false, - "itemCount": 6, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title" - ], - "forbiddenLabels": [ - "score", - "status" - ] - } - ], - "diagnostics": [ - { - "probe": "create_type", - "micros": 40469, - "version": 5, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 77 - }, - "end": { - "line": 3, - "character": 80 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'int' can't be assigned to the parameter type 'String'. ", - "range": { - "end": { - "character": 80, - "line": 3 - }, - "start": { - "character": 77, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - } - ] - }, - { - "probe": "predicate_type", - "micros": 47620, - "version": 6, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 81 - }, - "end": { - "line": 3, - "character": 88 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'String' can't be assigned to the parameter type 'int'. ", - "range": { - "end": { - "character": 88, - "line": 3 - }, - "start": { - "character": 81, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "selected_result", - "micros": 60539, - "version": 7, - "expectedCode": "invalid_assignment", - "expectedRange": { - "start": { - "line": 3, - "character": 124 - }, - "end": { - "line": 3, - "character": 134 - } - }, - "diagnostics": [ - { - "code": "invalid_assignment", - "message": "A value of type 'String' can't be assigned to a variable of type 'int'.\nTry changing the type of the variable, or casting the right-hand type to 'int'.", - "range": { - "end": { - "character": 134, - "line": 3 - }, - "start": { - "character": 124, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "missing_selected_field", - "micros": 45383, - "version": 8, - "expectedCode": "undefined_getter", - "expectedRange": { - "start": { - "line": 3, - "character": 169 - }, - "end": { - "line": 3, - "character": 174 - } - }, - "diagnostics": [ - { - "code": "undefined_getter", - "message": "The getter 'score' isn't defined for the type '({int id, String title})'.\nTry importing the library that defines 'score', correcting the name to the name of an existing getter, or defining a getter or field named 'score'.", - "range": { - "end": { - "character": 174, - "line": 3 - }, - "start": { - "character": 169, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - } - ], - "repairMicros": 52492, - "renameServerStartupMicros": 306917, - "symbolRenames": [ - { - "probe": "record_field", - "status": "unavailable", - "micros": 11100, - "prepare": null, - "error": null, - "workspaceEdit": null, - "applied": null - }, - { - "probe": "primary_constructor_field", - "status": "applied", - "micros": 788454, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 46, - "line": 12 - }, - "start": { - "character": 42, - "line": 12 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 1 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final name = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.name;\n" - } - } - }, - { - "probe": "table_field", - "status": "applied", - "micros": 22850, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 54, - "line": 13 - }, - "start": { - "character": 50, - "line": 13 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 2 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - }, - { - "probe": "record_alias", - "status": "applied", - "micros": 4802, - "prepare": { - "placeholder": "RecordUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 29, - "line": 9 - }, - "start": { - "character": 19, - "line": 9 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 10, - "line": 11 - }, - "start": { - "character": 0, - "line": 11 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 3 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 3, - "result": "import 'package:orm/orm.dart';\ntypedef RenamedUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RenamedUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRenamedUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - } - ], - "ormRule": { - "dartAnalysisMilliseconds": 180, - "dartAnalysisOutput": "Analyzing schema.dart...\nNo issues found!\n\n", - "generatorError": "Key selectors accept direct fields or positional records of fields only. (offset 162654)" - }, - "finalAnalyzeMilliseconds": 3540 - } - ] -} diff --git a/research/benchmarks/generation.json b/research/benchmarks/generation.json deleted file mode 100644 index defa9439..00000000 --- a/research/benchmarks/generation.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "recorded_at_utc": "2026-09-14T20:20:37.160791Z", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "cpu": "Apple M3 Max", - "logical_processors": 14, - "packages": { - "build_runner": "2.16.1", - "build": "4.0.11", - "dart_style": "3.1.13", - "analyzer": "14.4.0" - }, - "method": "One sample per case in a fresh consumer package; dependency download excluded; shared SDK/pub/native caches warm. Timings include process startup or filesystem watch detection as applicable. No p50/p95 claim.", - "results": [ - { - "models": 10, - "builder_mode": "aot", - "schema_bytes": 1622, - "generated_dart_bytes": 23308, - "cold_build_ms": 16201, - "no_change_build_ms": 566, - "watch_start_ms": 801, - "watch_field_edit_ms": 517, - "watch_import_edit_ms": 396, - "unrelated_edit_triggered_build": false, - "unrelated_observation_ms": 1000, - "analyze_ms": 480 - }, - { - "models": 100, - "builder_mode": "aot", - "schema_bytes": 15932, - "generated_dart_bytes": 231298, - "cold_build_ms": 16261, - "no_change_build_ms": 567, - "watch_start_ms": 775, - "watch_field_edit_ms": 647, - "watch_import_edit_ms": 658, - "unrelated_edit_triggered_build": false, - "unrelated_observation_ms": 1000, - "analyze_ms": 495 - }, - { - "models": 1000, - "builder_mode": "aot", - "schema_bytes": 162632, - "generated_dart_bytes": 2343598, - "cold_build_ms": 17955, - "no_change_build_ms": 581, - "watch_start_ms": 787, - "watch_field_edit_ms": 2169, - "watch_import_edit_ms": 2220, - "unrelated_edit_triggered_build": false, - "unrelated_observation_ms": 1000, - "analyze_ms": 3480 - } - ] -} diff --git a/research/benchmarks/runtime-selection.json b/research/benchmarks/runtime-selection.json deleted file mode 100644 index a81758b7..00000000 --- a/research/benchmarks/runtime-selection.json +++ /dev/null @@ -1,17884 +0,0 @@ -{ - "recordedAt": "2026-09-15T07:11:37.302015Z", - "sourceCommit": "d0a8c9a3c5c2bbd8feb99007d7723cd81a890ea5", - "harnessSha256": { - "tool/benchmark_runtime.dart": "4f7de193a9814bf571c35d672c71b0e04182b3de184a234f8979651c1d7bac10", - "tool/src/runtime_cases.dart": "fbd025c22388055452994d476d2ca1118bf123537d7c5f09a35c887d8887608c", - "tool/src/runtime_relay.dart": "cb6eb51792a6993c0143eea08f9f5bb9a6ebb31a3da32821047371709cde44cc", - "tool/src/runtime_vm.dart": "9a1f4f87be3371f9cee2d9a093def47cdf4679764d69ac219ba218a3a0931ca5" - }, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "cpu": "Apple M3 Max", - "samplesPerLane": 200, - "warmupPairs": 20, - "smoke": false, - "scope": "Warm native JIT, reused query objects; raw uses the same public Driver and precompiled identical SQL with manual same-codec mapping. ORM includes compilation. Default hooks and allocation tracing are disabled in timing. Loopback relay has no loss or bandwidth limit; delayed scenario is controlled TCP latency, not a remote-server measurement. PostgreSQL protocol bytes exclude TCP/IP headers. SQLite byte counts are UTF-8 JSON of driver rows, not IPC wire bytes. Heap fields are live isolate-group memory; traces count selected classes only, not total allocated bytes.", - "fixture": { - "users": 100, - "postsPerUser": 10, - "selectedPostsPerUser": 3, - "nullableNicknames": true, - "sqlite": "temporary file, WAL, warm cache", - "postgresPool": 4, - "concurrentClients": 8 - }, - "results": [ - { - "scenario": "sqlite_wal", - "environment": { - "backend": "sqlite_wal", - "databaseVersion": "3.53.4", - "pid": 5121 - }, - "oneWayDelayMs": 0, - "selectOneMicros": { - "samplesMicros": [ - 422, - 50, - 44, - 40, - 40, - 41, - 34, - 31, - 33, - 31 - ], - "p50Micros": 40, - "p95Micros": 422, - "minMicros": 31, - "maxMicros": 422, - "wallMicros": 766, - "operationsPerSecond": 13054.830287206267 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 273, - 172, - 170, - 170, - 166, - 180, - 174, - 175, - 186, - 208, - 190, - 369, - 177, - 196, - 173, - 213, - 187, - 165, - 158, - 161, - 172, - 164, - 159, - 164, - 166, - 185, - 184, - 134, - 146, - 144, - 128, - 133, - 127, - 136, - 169, - 141, - 135, - 129, - 138, - 135, - 134, - 151, - 151, - 173, - 142, - 129, - 137, - 132, - 144, - 133, - 419, - 248, - 167, - 147, - 144, - 127, - 121, - 218, - 142, - 127, - 137, - 121, - 121, - 148, - 216, - 144, - 133, - 129, - 162, - 125, - 167, - 115, - 105, - 151, - 186, - 139, - 127, - 115, - 111, - 126, - 111, - 118, - 105, - 143, - 122, - 112, - 103, - 110, - 103, - 178, - 134, - 111, - 106, - 110, - 108, - 115, - 127, - 121, - 149, - 115, - 114, - 335, - 170, - 130, - 185, - 118, - 127, - 120, - 111, - 103, - 101, - 111, - 115, - 109, - 115, - 114, - 114, - 115, - 122, - 110, - 114, - 107, - 117, - 111, - 105, - 120, - 103, - 109, - 113, - 122, - 150, - 232, - 222, - 115, - 120, - 211, - 132, - 111, - 137, - 126, - 119, - 117, - 118, - 106, - 126, - 109, - 119, - 114, - 109, - 115, - 110, - 130, - 116, - 121, - 113, - 110, - 108, - 153, - 129, - 125, - 118, - 113, - 113, - 115, - 195, - 115, - 117, - 132, - 125, - 118, - 110, - 116, - 108, - 117, - 108, - 117, - 109, - 110, - 116, - 134, - 131, - 141, - 120, - 142, - 114, - 119, - 118, - 173, - 161, - 115, - 114, - 134, - 111, - 123, - 127, - 118, - 109, - 129, - 112, - 124 - ], - "p50Micros": 127, - "p95Micros": 211, - "minMicros": 101, - "maxMicros": 419, - "wallMicros": 27941, - "operationsPerSecond": 7157.939944883862 - }, - "orm": { - "samplesMicros": [ - 353, - 243, - 209, - 202, - 230, - 213, - 225, - 216, - 224, - 267, - 260, - 212, - 231, - 261, - 288, - 294, - 224, - 227, - 297, - 214, - 231, - 247, - 232, - 222, - 231, - 310, - 202, - 192, - 197, - 199, - 176, - 200, - 180, - 184, - 204, - 191, - 171, - 180, - 183, - 172, - 185, - 538, - 210, - 218, - 186, - 170, - 179, - 173, - 179, - 169, - 190, - 182, - 256, - 227, - 194, - 181, - 191, - 325, - 227, - 205, - 186, - 170, - 192, - 183, - 204, - 193, - 161, - 156, - 245, - 171, - 2889, - 175, - 276, - 191, - 194, - 187, - 173, - 157, - 152, - 210, - 169, - 151, - 154, - 159, - 289, - 168, - 156, - 147, - 150, - 162, - 161, - 148, - 158, - 150, - 152, - 164, - 152, - 147, - 155, - 148, - 168, - 199, - 230, - 191, - 167, - 160, - 211, - 164, - 148, - 143, - 148, - 155, - 142, - 165, - 146, - 158, - 146, - 142, - 156, - 161, - 166, - 148, - 157, - 141, - 177, - 195, - 142, - 148, - 164, - 145, - 199, - 195, - 215, - 165, - 155, - 166, - 188, - 172, - 153, - 161, - 196, - 163, - 165, - 166, - 164, - 161, - 167, - 165, - 154, - 151, - 175, - 184, - 160, - 162, - 162, - 169, - 158, - 173, - 181, - 157, - 174, - 147, - 144, - 250, - 190, - 166, - 166, - 240, - 161, - 158, - 151, - 150, - 153, - 150, - 149, - 148, - 150, - 146, - 153, - 153, - 154, - 145, - 169, - 187, - 167, - 154, - 163, - 153, - 167, - 156, - 179, - 178, - 178, - 168, - 155, - 149, - 161, - 167, - 245, - 160 - ], - "p50Micros": 170, - "p95Micros": 267, - "minMicros": 141, - "maxMicros": 2889, - "wallMicros": 39723, - "operationsPerSecond": 5034.866450167409 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 183, - 324, - 463, - 589, - 736, - 852, - 973, - 1086, - 1020, - 1008, - 1000, - 993, - 961, - 960, - 962, - 962, - 952, - 948, - 933, - 932, - 930, - 930, - 935, - 943, - 1090, - 1156, - 1162, - 1159, - 1172, - 1169, - 1195, - 1215, - 1081, - 1014, - 1021, - 1055, - 1067, - 1076, - 1048, - 1025, - 1013, - 1017, - 1009, - 1013, - 1025, - 1033, - 1032, - 1037, - 1038, - 1036, - 1026, - 982, - 938, - 919, - 901, - 888, - 881, - 874, - 893, - 892, - 892, - 893, - 893, - 993, - 1003, - 1012, - 1002, - 1000, - 996, - 993, - 989, - 885, - 873, - 910, - 904, - 906, - 908, - 913, - 956, - 1008, - 1074, - 1027, - 1026, - 1058, - 1061, - 1058, - 1017, - 968, - 908, - 911, - 912, - 989, - 1032, - 1041, - 1047, - 1051, - 1070, - 1204, - 1256, - 1156, - 1126, - 1138, - 1229, - 1227, - 1208, - 1070, - 1011, - 999, - 986, - 963, - 863, - 863, - 883, - 887, - 906, - 915, - 914, - 923, - 923, - 925, - 904, - 900, - 889, - 882, - 885, - 880, - 991, - 991, - 992, - 992, - 991, - 992, - 991, - 994, - 887, - 886, - 887, - 910, - 904, - 898, - 890, - 884, - 893, - 892, - 915, - 918, - 934, - 948, - 960, - 984, - 978, - 972, - 941, - 925, - 936, - 931, - 926, - 917, - 934, - 957, - 984, - 978, - 965, - 966, - 971, - 968, - 988, - 1105, - 1145, - 1177, - 1191, - 1196, - 1193, - 1200, - 1167, - 1037, - 978, - 949, - 936, - 925, - 929, - 914, - 914, - 924, - 924, - 922, - 924, - 923, - 923, - 926, - 1024, - 1044, - 1053, - 1120, - 1130, - 1139, - 1143, - 1134, - 1031, - 992 - ], - "p50Micros": 973, - "p95Micros": 1191, - "minMicros": 183, - "maxMicros": 1256, - "wallMicros": 25151, - "operationsPerSecond": 7951.970100592422 - }, - "orm": { - "samplesMicros": [ - 436, - 564, - 744, - 890, - 1090, - 1239, - 1404, - 1613, - 1393, - 1389, - 1352, - 1440, - 1404, - 1433, - 1415, - 1376, - 1398, - 1441, - 1452, - 1375, - 1357, - 1328, - 1327, - 1326, - 1271, - 1223, - 1202, - 1297, - 1301, - 1340, - 1347, - 1327, - 1332, - 1349, - 1357, - 1258, - 1327, - 1297, - 1294, - 1289, - 1289, - 1366, - 1379, - 1367, - 1289, - 1269, - 1352, - 1352, - 1355, - 1273, - 1270, - 1259, - 1276, - 1316, - 1235, - 1354, - 1417, - 1428, - 1462, - 1485, - 1494, - 1460, - 1462, - 1344, - 1302, - 1282, - 1269, - 1275, - 1259, - 1248, - 1231, - 1220, - 1201, - 1286, - 1401, - 1503, - 1543, - 1576, - 1577, - 1581, - 1586, - 1511, - 1394, - 1284, - 1256, - 1249, - 1252, - 1265, - 1242, - 1260, - 1241, - 1243, - 1241, - 1241, - 1245, - 1231, - 1234, - 1242, - 1273, - 1267, - 1253, - 1230, - 1235, - 1253, - 1366, - 1306, - 1283, - 1285, - 1294, - 1300, - 1294, - 1278, - 1172, - 1203, - 1292, - 1296, - 1347, - 1377, - 1407, - 1429, - 1440, - 1421, - 1342, - 1345, - 1309, - 1272, - 1247, - 1244, - 1235, - 1248, - 1228, - 1207, - 1170, - 1169, - 1163, - 1156, - 1151, - 1147, - 1142, - 1144, - 1152, - 1164, - 1161, - 1149, - 1150, - 1134, - 1118, - 1129, - 1122, - 1115, - 1121, - 1123, - 1118, - 1125, - 1146, - 1138, - 1148, - 1155, - 1149, - 1150, - 1152, - 1150, - 1282, - 1274, - 1292, - 1288, - 1297, - 1293, - 1299, - 1311, - 1256, - 1267, - 1276, - 1293, - 1291, - 1304, - 1292, - 1297, - 1217, - 1219, - 1191, - 1181, - 1174, - 1164, - 1173, - 1156, - 1154, - 1150, - 1147, - 1137, - 1143, - 1144, - 1142, - 1135, - 1170, - 1203, - 1232, - 1241, - 1242, - 1212 - ], - "p50Micros": 1270, - "p95Micros": 1462, - "minMicros": 436, - "maxMicros": 1613, - "wallMicros": 32250, - "operationsPerSecond": 6201.550387596899 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 29, - 162, - 278, - 389, - 497, - 605, - 712, - 820, - 766, - 763, - 809, - 827, - 839, - 850, - 1006, - 1098, - 1091, - 1066, - 1500, - 1521, - 1735, - 1626, - 1645, - 1665, - 1684, - 1248, - 1269, - 1072, - 1035, - 959, - 961, - 1030, - 1003, - 980, - 1034, - 1044, - 1113, - 1100, - 1013, - 1013, - 1030, - 943, - 911, - 831, - 813, - 807, - 793, - 778, - 776, - 775, - 767, - 766, - 764, - 760, - 757, - 761, - 768, - 771, - 774, - 779, - 782, - 784, - 771, - 763 - ], - "p50Micros": 827, - "p95Micros": 1645, - "minMicros": 29, - "maxMicros": 1735, - "wallMicros": 59081 - }, - "sqlMicros": [ - 117, - 108, - 103, - 101, - 100, - 99, - 99, - 99, - 104, - 148, - 117, - 108, - 107, - 252, - 181, - 110, - 113, - 100, - 130, - 324, - 160, - 195, - 131, - 125, - 116, - 150, - 129, - 122, - 118, - 131, - 192, - 116, - 101, - 186, - 129, - 186, - 117, - 105, - 111, - 115, - 104, - 99, - 108, - 100, - 99, - 101, - 100, - 101, - 98, - 100, - 99, - 99, - 99, - 99, - 106, - 107, - 105, - 103, - 103, - 101, - 101, - 97, - 97, - 98 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 41, - 285, - 449, - 603, - 942, - 1139, - 1275, - 1415, - 1260, - 1238, - 1251, - 1040, - 1030, - 1347, - 1505, - 2128, - 2136, - 2124, - 2124, - 2100, - 1759, - 1609, - 992, - 965, - 951, - 949, - 939, - 946, - 953, - 960, - 968, - 971, - 968, - 971, - 957, - 933, - 927, - 915, - 912, - 905, - 1158, - 1161, - 1164, - 1191, - 2280, - 2313, - 2340, - 2171, - 2451, - 2600, - 1570, - 1610, - 1670, - 1670, - 1609, - 1449, - 1348, - 1291, - 1267, - 1185, - 1163, - 1096, - 968, - 926 - ], - "p50Micros": 1163, - "p95Micros": 2313, - "minMicros": 41, - "maxMicros": 2600, - "wallMicros": 83533 - }, - "sqlMicros": [ - 129, - 132, - 115, - 309, - 159, - 111, - 110, - 115, - 116, - 127, - 112, - 133, - 430, - 250, - 723, - 128, - 118, - 115, - 112, - 111, - 111, - 111, - 107, - 109, - 110, - 105, - 117, - 122, - 111, - 114, - 108, - 109, - 107, - 106, - 106, - 107, - 105, - 105, - 104, - 104, - 110, - 110, - 133, - 138, - 127, - 119, - 185, - 376, - 242, - 157, - 143, - 195, - 123, - 135, - 221, - 134, - 132, - 115, - 110, - 103, - 101, - 99, - 99, - 110 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 35 - ], - "sqlMicros": [ - 178 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 15 - ], - "sqlMicros": [ - 156 - ], - "ormDecodeMicros": [ - 59 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367474824613, - "endMicros": 1367474825696, - "retainedRootRows": 100, - "rssBefore": 308183040, - "rssAfter": 308920320, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14620096, - "heapCapacity": 27852800, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291448", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 5104 - }, - { - "name": "_Map", - "instancesCurrent": 14, - "bytesCurrent": 896 - }, - { - "name": "_List", - "instancesCurrent": 40142, - "bytesCurrent": 3105328 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1130, - "bytesCurrent": 36160 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14682416, - "heapCapacity": 25542656, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291611", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 9904 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40267, - "bytesCurrent": 3115072 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 343, - "counts": { - "_List": 34, - "_Record": 303, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_allocateData", - "samples": 18 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_worker", - "samples": 3 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_GrowableList._withData", - "samples": 3 - }, - { - "function": "_List", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367475017206, - "endMicros": 1367475018633, - "retainedRootRows": 100, - "rssBefore": 315097088, - "rssAfter": 315932672, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14665648, - "heapCapacity": 25542656, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291648", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 5104 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40163, - "bytesCurrent": 3106464 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1128, - "bytesCurrent": 36096 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14682576, - "heapCapacity": 26066944, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291808", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 9904 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40267, - "bytesCurrent": 3115072 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 663, - "counts": { - "_List": 204, - "_SelectionPlan": 3, - "_GrowableList": 129, - "_Map": 18, - "_Record": 309 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_GrowableList._withData", - "samples": 120 - }, - { - "function": "_allocateData", - "samples": 48 - }, - { - "function": "quote", - "samples": 48 - }, - { - "function": "_write", - "samples": 33 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "_List", - "samples": 20 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_addPart", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_plan", - "samples": 6 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 164, - 58, - 55, - 54, - 53, - 52, - 50, - 53, - 55, - 53, - 52, - 51, - 49, - 51, - 50, - 54, - 53, - 52, - 52, - 49, - 48, - 52, - 54, - 52, - 52, - 54, - 54, - 54, - 53, - 54, - 57, - 55, - 56, - 158, - 105, - 68, - 76, - 96, - 61, - 56, - 54, - 58, - 59, - 219, - 71, - 82, - 71, - 60, - 71, - 176, - 86, - 124, - 86, - 73, - 87, - 109, - 80, - 119, - 57, - 50, - 50, - 50, - 49, - 55, - 149, - 131, - 246, - 68, - 62, - 52, - 51, - 53, - 50, - 57, - 52, - 63, - 52, - 63, - 58, - 63, - 65, - 56, - 55, - 60, - 60, - 52, - 56, - 66, - 56, - 61, - 106, - 73, - 53, - 61, - 69, - 64, - 60, - 75, - 66, - 61, - 57, - 93, - 53, - 50, - 51, - 68, - 52, - 57, - 63, - 64, - 102, - 73, - 81, - 56, - 53, - 57, - 72, - 64, - 54, - 79, - 120, - 77, - 58, - 59, - 63, - 59, - 61, - 50, - 50, - 51, - 64, - 61, - 54, - 59, - 56, - 63, - 63, - 75, - 65, - 63, - 64, - 61, - 63, - 63, - 63, - 67, - 66, - 63, - 57, - 67, - 62, - 53, - 56, - 49, - 50, - 53, - 53, - 53, - 53, - 60, - 63, - 53, - 63, - 57, - 69, - 62, - 64, - 64, - 133, - 54, - 54, - 50, - 48, - 50, - 49, - 49, - 48, - 48, - 48, - 48, - 49, - 48, - 49, - 48, - 70, - 65, - 69, - 63, - 58, - 50, - 51, - 52, - 55, - 50, - 52, - 50, - 51, - 66, - 67, - 53 - ], - "p50Micros": 57, - "p95Micros": 119, - "minMicros": 48, - "maxMicros": 246, - "wallMicros": 13052, - "operationsPerSecond": 15323.322096230462 - }, - "orm": { - "samplesMicros": [ - 129, - 87, - 80, - 74, - 70, - 68, - 68, - 68, - 72, - 71, - 68, - 70, - 68, - 68, - 67, - 71, - 70, - 69, - 69, - 66, - 66, - 69, - 70, - 71, - 71, - 70, - 71, - 69, - 70, - 71, - 73, - 70, - 81, - 1110, - 151, - 127, - 894, - 1271, - 143, - 79, - 82, - 78, - 92, - 144, - 105, - 89, - 80, - 79, - 180, - 200, - 104, - 164, - 106, - 118, - 118, - 85, - 146, - 99, - 84, - 65, - 65, - 66, - 62, - 72, - 194, - 276, - 164, - 101, - 80, - 80, - 71, - 71, - 73, - 75, - 84, - 76, - 66, - 75, - 80, - 75, - 82, - 82, - 72, - 80, - 66, - 69, - 64, - 196, - 77, - 131, - 143, - 90, - 107, - 76, - 76, - 80, - 69, - 96, - 83, - 75, - 71, - 79, - 68, - 65, - 99, - 88, - 81, - 66, - 84, - 76, - 97, - 94, - 86, - 69, - 78, - 67, - 104, - 92, - 68, - 75, - 107, - 85, - 78, - 86, - 73, - 79, - 72, - 64, - 67, - 67, - 64, - 73, - 67, - 68, - 75, - 79, - 76, - 72, - 76, - 70, - 78, - 75, - 79, - 74, - 184, - 98, - 72, - 85, - 78, - 78, - 71, - 75, - 68, - 73, - 70, - 81, - 67, - 74, - 77, - 77, - 67, - 77, - 72, - 81, - 67, - 80, - 105, - 85, - 108, - 73, - 64, - 62, - 59, - 60, - 59, - 59, - 58, - 59, - 59, - 59, - 59, - 59, - 58, - 58, - 248, - 94, - 75, - 75, - 68, - 73, - 72, - 76, - 70, - 67, - 74, - 64, - 65, - 74, - 77, - 65 - ], - "p50Micros": 75, - "p95Micros": 164, - "minMicros": 58, - "maxMicros": 1271, - "wallMicros": 19831, - "operationsPerSecond": 10085.2201099289 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 214, - 339, - 436, - 514, - 647, - 705, - 764, - 825, - 682, - 613, - 584, - 566, - 492, - 496, - 497, - 516, - 526, - 526, - 532, - 546, - 567, - 574, - 577, - 564, - 564, - 566, - 564, - 584, - 580, - 590, - 597, - 701, - 785, - 791, - 787, - 765, - 752, - 733, - 717, - 666, - 594, - 588, - 592, - 577, - 572, - 607, - 613, - 581, - 571, - 606, - 601, - 611, - 679, - 756, - 771, - 736, - 723, - 739, - 743, - 729, - 666, - 548, - 528, - 525, - 529, - 466, - 458, - 456, - 446, - 442, - 455, - 475, - 471, - 477, - 475, - 474, - 471, - 481, - 463, - 444, - 448, - 453, - 451, - 453, - 472, - 501, - 501, - 501, - 497, - 489, - 491, - 510, - 502, - 475, - 487, - 496, - 499, - 506, - 507, - 487, - 486, - 487, - 505, - 509, - 510, - 513, - 516, - 539, - 549, - 539, - 517, - 504, - 494, - 530, - 536, - 524, - 514, - 522, - 526, - 526, - 536, - 512, - 523, - 527, - 630, - 649, - 657, - 668, - 668, - 648, - 630, - 618, - 503, - 485, - 468, - 456, - 446, - 442, - 430, - 433, - 438, - 426, - 424, - 426, - 427, - 423, - 432, - 425, - 422, - 428, - 435, - 440, - 439, - 441, - 432, - 433, - 433, - 429, - 435, - 438, - 438, - 443, - 454, - 459, - 460, - 474, - 523, - 531, - 572, - 573, - 583, - 590, - 589, - 576, - 521, - 513, - 474, - 472, - 460, - 447, - 451, - 468, - 478, - 492, - 495, - 500, - 509, - 525, - 533, - 517, - 504, - 498, - 547, - 551, - 540, - 531, - 533, - 538, - 541, - 529 - ], - "p50Micros": 514, - "p95Micros": 739, - "minMicros": 214, - "maxMicros": 825, - "wallMicros": 13601, - "operationsPerSecond": 14704.801117564884 - }, - "orm": { - "samplesMicros": [ - 162, - 235, - 318, - 397, - 471, - 545, - 616, - 686, - 650, - 660, - 658, - 677, - 677, - 685, - 708, - 757, - 727, - 719, - 712, - 685, - 693, - 712, - 693, - 641, - 632, - 614, - 604, - 598, - 581, - 546, - 534, - 531, - 530, - 531, - 531, - 530, - 530, - 529, - 529, - 529, - 529, - 591, - 591, - 594, - 604, - 629, - 630, - 640, - 646, - 587, - 586, - 588, - 590, - 576, - 586, - 591, - 598, - 605, - 606, - 612, - 630, - 623, - 640, - 628, - 676, - 664, - 665, - 653, - 620, - 614, - 592, - 589, - 532, - 539, - 533, - 550, - 565, - 576, - 569, - 642, - 733, - 772, - 807, - 799, - 795, - 793, - 793, - 724, - 664, - 643, - 649, - 658, - 661, - 657, - 670, - 663, - 626, - 600, - 575, - 821, - 859, - 861, - 859, - 859, - 863, - 864, - 859, - 607, - 553, - 544, - 531, - 533, - 539, - 547, - 554, - 548, - 553, - 561, - 566, - 573, - 574, - 577, - 568, - 574, - 579, - 581, - 590, - 593, - 601, - 593, - 600, - 600, - 605, - 603, - 603, - 598, - 581, - 614, - 626, - 628, - 634, - 669, - 668, - 677, - 685, - 695, - 709, - 710, - 736, - 826, - 831, - 826, - 832, - 795, - 765, - 757, - 715, - 585, - 571, - 570, - 561, - 547, - 535, - 526, - 530, - 566, - 574, - 570, - 584, - 593, - 602, - 603, - 600, - 570, - 571, - 567, - 567, - 567, - 570, - 577, - 581, - 579, - 670, - 693, - 684, - 690, - 719, - 703, - 703, - 696, - 596, - 576, - 587, - 582, - 554, - 599, - 606, - 615, - 619, - 612 - ], - "p50Micros": 603, - "p95Micros": 826, - "minMicros": 162, - "maxMicros": 864, - "wallMicros": 15914, - "operationsPerSecond": 12567.550584391101 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 25, - 97, - 149, - 201, - 251, - 301, - 350, - 399, - 357, - 354, - 360, - 365, - 370, - 386, - 392, - 390, - 390, - 382, - 383, - 396, - 378, - 381, - 384, - 384, - 383, - 381, - 374, - 379, - 379, - 381, - 384, - 395, - 393, - 387, - 384, - 383, - 381, - 380, - 447, - 448, - 447, - 446, - 444, - 443, - 442, - 366, - 364, - 364, - 363, - 363, - 364, - 364, - 364, - 365, - 364, - 365, - 364, - 363, - 362, - 362, - 360, - 360, - 358, - 358 - ], - "p50Micros": 374, - "p95Micros": 446, - "minMicros": 25, - "maxMicros": 448, - "wallMicros": 23199 - }, - "sqlMicros": [ - 68, - 49, - 47, - 46, - 46, - 45, - 46, - 49, - 46, - 54, - 51, - 51, - 60, - 51, - 47, - 46, - 45, - 52, - 63, - 47, - 51, - 50, - 46, - 45, - 51, - 56, - 52, - 50, - 51, - 50, - 56, - 49, - 49, - 49, - 49, - 49, - 49, - 122, - 50, - 48, - 48, - 48, - 48, - 47, - 47, - 48, - 48, - 47, - 48, - 49, - 47, - 48, - 48, - 47, - 48, - 48, - 48, - 47, - 47, - 46, - 47, - 47, - 47, - 119 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 22, - 107, - 190, - 277, - 356, - 425, - 494, - 562, - 519, - 500, - 494, - 485, - 481, - 478, - 480, - 474, - 469, - 450, - 471, - 468, - 482, - 494, - 496, - 495, - 477, - 466, - 460, - 437, - 430, - 441, - 441, - 442, - 442, - 452, - 459, - 457, - 476, - 527, - 705, - 746, - 765, - 850, - 873, - 794, - 842, - 678, - 658, - 662, - 589, - 622, - 614, - 687, - 691, - 687, - 714, - 720, - 685, - 685, - 641, - 644, - 640, - 573, - 553, - 520 - ], - "p50Micros": 494, - "p95Micros": 794, - "minMicros": 22, - "maxMicros": 873, - "wallMicros": 34414 - }, - "sqlMicros": [ - 65, - 64, - 68, - 64, - 53, - 52, - 52, - 52, - 52, - 60, - 58, - 50, - 51, - 54, - 48, - 48, - 55, - 59, - 47, - 58, - 60, - 49, - 47, - 47, - 47, - 48, - 47, - 51, - 57, - 47, - 47, - 47, - 57, - 53, - 53, - 74, - 87, - 219, - 72, - 74, - 137, - 68, - 73, - 63, - 61, - 58, - 73, - 63, - 96, - 67, - 131, - 64, - 53, - 109, - 63, - 77, - 65, - 88, - 60, - 48, - 46, - 52, - 45, - 44 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 9 - ], - "sqlMicros": [ - 54 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 11 - ], - "sqlMicros": [ - 74 - ], - "ormDecodeMicros": [ - 6 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367475218464, - "endMicros": 1367475219170, - "retainedRootRows": 100, - "rssBefore": 324108288, - "rssAfter": 324747264, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14601120, - "heapCapacity": 21872640, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291849", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 3504 - }, - { - "name": "_Map", - "instancesCurrent": 14, - "bytesCurrent": 896 - }, - { - "name": "_List", - "instancesCurrent": 40187, - "bytesCurrent": 3104800 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1130, - "bytesCurrent": 36160 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14618736, - "heapCapacity": 21872640, - "externalUsage": 80 - }, - "gcTimestamp": "1789456291997", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 6704 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40291, - "bytesCurrent": 3111760 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 340, - "counts": { - "_List": 31, - "_Record": 303, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_allocateData", - "samples": 18 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_GrowableList._withData", - "samples": 3 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367475403349, - "endMicros": 1367475404314, - "retainedRootRows": 100, - "rssBefore": 334610432, - "rssAfter": 335331328, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14606416, - "heapCapacity": 21872640, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292031", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 3504 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40190, - "bytesCurrent": 3104992 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1128, - "bytesCurrent": 36096 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14620080, - "heapCapacity": 21872640, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292183", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 6704 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40294, - "bytesCurrent": 3111968 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 612, - "counts": { - "_List": 171, - "_SelectionPlan": 3, - "_GrowableList": 111, - "_Map": 18, - "_Record": 309 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_allocateData", - "samples": 129 - }, - { - "function": "quote", - "samples": 36 - }, - { - "function": "_write", - "samples": 27 - }, - { - "function": "_List", - "samples": 20 - }, - { - "function": "writeSql", - "samples": 15 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_addPart", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_plan", - "samples": 6 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 917, - 908, - 842, - 872, - 768, - 802, - 806, - 836, - 847, - 920, - 835, - 944, - 951, - 796, - 790, - 836, - 952, - 845, - 805, - 839, - 910, - 812, - 821, - 786, - 803, - 1109, - 871, - 786, - 781, - 921, - 885, - 807, - 786, - 872, - 806, - 774, - 767, - 818, - 888, - 855, - 801, - 779, - 775, - 859, - 840, - 823, - 819, - 821, - 960, - 788, - 818, - 804, - 807, - 897, - 809, - 796, - 789, - 831, - 874, - 829, - 793, - 832, - 809, - 854, - 799, - 786, - 781, - 1002, - 902, - 793, - 785, - 835, - 850, - 788, - 795, - 782, - 864, - 844, - 773, - 790, - 789, - 815, - 851, - 813, - 803, - 923, - 807, - 812, - 817, - 782, - 790, - 935, - 879, - 763, - 763, - 821, - 806, - 768, - 804, - 775, - 821, - 874, - 766, - 775, - 771, - 752, - 846, - 807, - 804, - 790, - 806, - 1097, - 811, - 808, - 905, - 937, - 791, - 787, - 776, - 788, - 895, - 771, - 780, - 892, - 821, - 765, - 745, - 784, - 760, - 827, - 819, - 789, - 769, - 754, - 767, - 826, - 794, - 777, - 771, - 947, - 915, - 747, - 981, - 781, - 856, - 742, - 773, - 759, - 771, - 818, - 863, - 841, - 792, - 810, - 854, - 785, - 802, - 813, - 806, - 850, - 814, - 790, - 753, - 806, - 829, - 802, - 783, - 789, - 812, - 857, - 779, - 766, - 736, - 770, - 862, - 783, - 770, - 845, - 789, - 794, - 904, - 737, - 827, - 822, - 793, - 768, - 766, - 791, - 793, - 845, - 792, - 764, - 772, - 816, - 820, - 769, - 758, - 786, - 871, - 804 - ], - "p50Micros": 806, - "p95Micros": 935, - "minMicros": 736, - "maxMicros": 1109, - "wallMicros": 164112, - "operationsPerSecond": 1218.6799259042605 - }, - "orm": { - "samplesMicros": [ - 1011, - 1355, - 1104, - 1082, - 974, - 948, - 1064, - 1164, - 1138, - 977, - 1143, - 994, - 973, - 912, - 1016, - 996, - 979, - 961, - 987, - 969, - 1001, - 1025, - 911, - 889, - 922, - 1151, - 898, - 861, - 910, - 998, - 921, - 919, - 892, - 917, - 1006, - 999, - 1099, - 922, - 1000, - 1063, - 913, - 871, - 889, - 1029, - 1037, - 912, - 848, - 839, - 1044, - 929, - 876, - 887, - 913, - 1012, - 870, - 937, - 916, - 1024, - 915, - 912, - 877, - 872, - 900, - 917, - 933, - 887, - 979, - 948, - 894, - 896, - 879, - 891, - 954, - 947, - 863, - 862, - 927, - 1059, - 936, - 897, - 898, - 901, - 948, - 904, - 876, - 865, - 1108, - 884, - 861, - 871, - 891, - 873, - 922, - 844, - 860, - 885, - 1071, - 962, - 867, - 858, - 886, - 991, - 871, - 872, - 981, - 912, - 958, - 907, - 863, - 859, - 924, - 1254, - 1016, - 1129, - 939, - 971, - 854, - 871, - 865, - 888, - 956, - 891, - 867, - 861, - 984, - 899, - 896, - 857, - 870, - 908, - 902, - 847, - 896, - 938, - 902, - 915, - 886, - 851, - 896, - 906, - 967, - 907, - 900, - 872, - 935, - 909, - 850, - 836, - 901, - 899, - 881, - 858, - 832, - 845, - 934, - 944, - 856, - 874, - 883, - 1132, - 915, - 949, - 826, - 886, - 974, - 894, - 866, - 853, - 1019, - 971, - 999, - 896, - 898, - 1006, - 898, - 887, - 851, - 864, - 904, - 925, - 908, - 809, - 884, - 987, - 891, - 888, - 852, - 872, - 918, - 925, - 843, - 836, - 866, - 928, - 887, - 897, - 853, - 864, - 931, - 991 - ], - "p50Micros": 907, - "p95Micros": 1099, - "minMicros": 809, - "maxMicros": 1355, - "wallMicros": 185956, - "operationsPerSecond": 1075.5232420572609 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 779, - 1528, - 2267, - 2995, - 3803, - 4597, - 5415, - 6299, - 6344, - 6375, - 6427, - 6471, - 6446, - 6434, - 6416, - 6321, - 6464, - 6541, - 6537, - 6521, - 6538, - 6704, - 6721, - 6709, - 6544, - 6510, - 6615, - 6757, - 6775, - 6574, - 6569, - 6635, - 6586, - 6528, - 6377, - 6283, - 6257, - 6475, - 6508, - 6472, - 6514, - 6514, - 6531, - 6662, - 6648, - 6421, - 6350, - 6486, - 6506, - 6498, - 6522, - 6377, - 6372, - 6411, - 6416, - 6266, - 6266, - 6328, - 6408, - 6453, - 6413, - 6414, - 6395, - 6366, - 6335, - 6292, - 6222, - 6221, - 6454, - 6515, - 6597, - 6716, - 6765, - 6747, - 6716, - 6690, - 6543, - 6542, - 6557, - 6546, - 6523, - 6530, - 6531, - 6552, - 6539, - 6464, - 6428, - 6326, - 6461, - 6607, - 6637, - 6621, - 6628, - 6655, - 6634, - 6656, - 6523, - 6495, - 6699, - 6730, - 6743, - 6777, - 6782, - 6767, - 6768, - 6666, - 6495, - 6464, - 6475, - 6541, - 6587, - 6597, - 6587, - 6587, - 6547, - 6524, - 6522, - 6550, - 6565, - 6595, - 6586, - 6599, - 6620, - 6632, - 6599, - 6468, - 6411, - 6540, - 6644, - 6694, - 6689, - 6670, - 6680, - 6683, - 6666, - 6502, - 6458, - 6434, - 6554, - 6634, - 6907, - 6928, - 6895, - 6874, - 6792, - 6735, - 6616, - 6545, - 6308, - 6384, - 6370, - 6400, - 6448, - 6446, - 6408, - 6384, - 6311, - 6208, - 6250, - 6289, - 6291, - 6330, - 6343, - 6363, - 6344, - 6323, - 6297, - 6245, - 6204, - 6208, - 6275, - 6330, - 6344, - 6365, - 6370, - 6351, - 6341, - 6293, - 6237, - 6185, - 6321, - 6437, - 6506, - 6484, - 6517, - 6539, - 6529, - 6521, - 6414, - 6324, - 6381, - 6507, - 6541, - 6557, - 6573, - 6567, - 6534, - 6503, - 6412, - 6340 - ], - "p50Micros": 6506, - "p95Micros": 6765, - "minMicros": 779, - "maxMicros": 6928, - "wallMicros": 162331, - "operationsPerSecond": 1232.0505633551202 - }, - "orm": { - "samplesMicros": [ - 1246, - 2177, - 3078, - 3954, - 5054, - 5896, - 6696, - 7536, - 7180, - 7124, - 7179, - 7206, - 6943, - 6961, - 6998, - 7017, - 6979, - 6965, - 6862, - 6866, - 6939, - 6948, - 6984, - 7341, - 7367, - 7340, - 7342, - 7281, - 7331, - 7453, - 7521, - 7199, - 7181, - 7153, - 7160, - 7181, - 7092, - 7123, - 7162, - 7211, - 7195, - 7202, - 7212, - 7218, - 7237, - 7101, - 6997, - 7151, - 7193, - 7170, - 7112, - 7036, - 6987, - 6938, - 6912, - 6684, - 6705, - 6817, - 6969, - 7013, - 7125, - 7138, - 7129, - 7119, - 7104, - 7098, - 7177, - 7249, - 7194, - 7182, - 7195, - 7167, - 7124, - 7076, - 6882, - 6906, - 6950, - 6992, - 7005, - 7037, - 7074, - 7077, - 7112, - 7051, - 7077, - 7471, - 7469, - 7433, - 7367, - 7342, - 7284, - 7238, - 7119, - 6700, - 6873, - 6931, - 6985, - 6960, - 6972, - 6949, - 6946, - 6984, - 6824, - 6851, - 6883, - 6918, - 6891, - 6898, - 6895, - 6845, - 6827, - 6766, - 6741, - 6840, - 6997, - 7074, - 7077, - 7098, - 7094, - 7089, - 7095, - 7021, - 6968, - 6982, - 7050, - 7051, - 7049, - 7044, - 7016, - 7005, - 6963, - 6963, - 7074, - 7139, - 7122, - 7108, - 7114, - 7132, - 7092, - 7008, - 6796, - 6735, - 6983, - 6994, - 6965, - 6958, - 6987, - 7019, - 7208, - 7207, - 6986, - 7048, - 7145, - 7169, - 7160, - 7148, - 7004, - 7038, - 7045, - 6972, - 7010, - 7126, - 7184, - 7171, - 7163, - 7083, - 7002, - 7000, - 6912, - 6784, - 6985, - 7204, - 7233, - 7267, - 7333, - 7353, - 7326, - 7335, - 7082, - 7063, - 7155, - 7184, - 7152, - 7116, - 7122, - 7078, - 7097, - 6942, - 6973, - 7032, - 7077, - 7119, - 7133, - 7128, - 7090, - 7111, - 6993, - 6906, - 6949, - 7013 - ], - "p50Micros": 7074, - "p95Micros": 7342, - "minMicros": 1246, - "maxMicros": 7536, - "wallMicros": 177342, - "operationsPerSecond": 1127.7644325653257 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 19, - 859, - 1663, - 2419, - 3212, - 4006, - 4839, - 5639, - 5668, - 5734, - 5754, - 5981, - 6011, - 6024, - 6033, - 6246, - 6258, - 6330, - 6219, - 6350, - 6336, - 6347, - 6318, - 6243, - 6161, - 6031, - 5864, - 5867, - 5934, - 5714, - 5694, - 5734, - 5720, - 5705, - 5639, - 5537, - 5478, - 5460, - 5613, - 5662, - 5696, - 5724, - 5703, - 5664, - 5669, - 5480, - 5465, - 5464, - 5485, - 5525, - 5567, - 5575, - 5593, - 5564, - 5521, - 5505, - 5487, - 5461, - 5464, - 5509, - 5554, - 5584, - 5555, - 5554 - ], - "p50Micros": 5639, - "p95Micros": 6330, - "minMicros": 19, - "maxMicros": 6350, - "wallMicros": 345689 - }, - "sqlMicros": [ - 59, - 716, - 48, - 665, - 52, - 650, - 48, - 692, - 49, - 692, - 48, - 732, - 48, - 698, - 48, - 791, - 51, - 765, - 48, - 673, - 48, - 922, - 47, - 713, - 61, - 729, - 56, - 699, - 51, - 997, - 80, - 737, - 51, - 735, - 65, - 797, - 108, - 785, - 54, - 716, - 49, - 716, - 315, - 706, - 55, - 703, - 47, - 685, - 50, - 700, - 50, - 704, - 47, - 746, - 52, - 798, - 60, - 751, - 49, - 695, - 51, - 718, - 49, - 689, - 49, - 691, - 48, - 684, - 49, - 707, - 57, - 717, - 42, - 688, - 50, - 875, - 57, - 728, - 53, - 721, - 50, - 707, - 49, - 685, - 49, - 687, - 47, - 690, - 47, - 690, - 48, - 716, - 48, - 724, - 44, - 735, - 50, - 721, - 55, - 722, - 48, - 695, - 47, - 707, - 47, - 693, - 47, - 684, - 47, - 716, - 51, - 704, - 50, - 702, - 49, - 701, - 48, - 748, - 47, - 741, - 49, - 714, - 46, - 689, - 49, - 705, - 47, - 685 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 10, - 901, - 1799, - 2719, - 3676, - 4665, - 5638, - 6503, - 6466, - 6408, - 6330, - 6167, - 6091, - 5984, - 6142, - 6206, - 6253, - 6269, - 6269, - 6219, - 6210, - 6036, - 5987, - 6065, - 6136, - 6216, - 6263, - 6275, - 6261, - 6232, - 6101, - 6058, - 6125, - 6263, - 6363, - 6415, - 6386, - 6379, - 6335, - 6159, - 6006, - 5908, - 6008, - 6150, - 6182, - 6160, - 6135, - 6108, - 6103, - 5942, - 5859, - 5872, - 6183, - 6301, - 6332, - 6356, - 6361, - 6345, - 6313, - 6061, - 6011, - 6169, - 6260, - 6259 - ], - "p50Micros": 6167, - "p95Micros": 6408, - "minMicros": 10, - "maxMicros": 6503, - "wallMicros": 372364 - }, - "sqlMicros": [ - 52, - 700, - 65, - 712, - 58, - 753, - 51, - 729, - 90, - 776, - 112, - 733, - 54, - 706, - 51, - 715, - 50, - 694, - 49, - 691, - 50, - 692, - 48, - 732, - 50, - 704, - 49, - 872, - 60, - 760, - 51, - 739, - 51, - 709, - 48, - 693, - 48, - 691, - 48, - 693, - 48, - 712, - 49, - 725, - 48, - 820, - 53, - 751, - 58, - 759, - 58, - 725, - 52, - 707, - 50, - 694, - 51, - 693, - 50, - 692, - 52, - 722, - 53, - 819, - 67, - 810, - 73, - 762, - 74, - 696, - 50, - 658, - 48, - 689, - 49, - 687, - 48, - 685, - 47, - 712, - 48, - 697, - 47, - 846, - 66, - 756, - 65, - 689, - 52, - 657, - 58, - 652, - 47, - 687, - 48, - 698, - 48, - 698, - 49, - 714, - 48, - 720, - 50, - 966, - 58, - 754, - 49, - 713, - 56, - 714, - 49, - 704, - 48, - 699, - 48, - 699, - 47, - 730, - 53, - 711, - 47, - 871, - 63, - 776, - 68, - 664, - 57, - 667 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38, ?39, ?40, ?41, ?42, ?43, ?44, ?45, ?46, ?47, ?48, ?49, ?50, ?51, ?52, ?53, ?54, ?55, ?56, ?57, ?58, ?59, ?60, ?61, ?62, ?63, ?64, ?65, ?66, ?67, ?68, ?69, ?70, ?71, ?72, ?73, ?74, ?75, ?76, ?77, ?78, ?79, ?80, ?81, ?82, ?83, ?84, ?85, ?86, ?87, ?88, ?89, ?90, ?91, ?92, ?93, ?94, ?95, ?96, ?97, ?98, ?99, ?100))) AS \"orm_partition\" WHERE \"c3\" > ?101 AND \"c3\" <= ?102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 8 - ], - "sqlMicros": [ - 63, - 661 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38, ?39, ?40, ?41, ?42, ?43, ?44, ?45, ?46, ?47, ?48, ?49, ?50, ?51, ?52, ?53, ?54, ?55, ?56, ?57, ?58, ?59, ?60, ?61, ?62, ?63, ?64, ?65, ?66, ?67, ?68, ?69, ?70, ?71, ?72, ?73, ?74, ?75, ?76, ?77, ?78, ?79, ?80, ?81, ?82, ?83, ?84, ?85, ?86, ?87, ?88, ?89, ?90, ?91, ?92, ?93, ?94, ?95, ?96, ?97, ?98, ?99, ?100))) AS \"orm_partition\" WHERE \"c3\" > ?101 AND \"c3\" <= ?102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 6 - ], - "sqlMicros": [ - 69, - 690 - ], - "ormDecodeMicros": [ - 42, - 5 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367475648529, - "endMicros": 1367475652655, - "retainedRootRows": 100, - "rssBefore": 343605248, - "rssAfter": 346456064, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14827712, - "heapCapacity": 25018368, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292274", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 408, - "bytesCurrent": 14672 - }, - { - "name": "_Map", - "instancesCurrent": 13, - "bytesCurrent": 832 - }, - { - "name": "_List", - "instancesCurrent": 40088, - "bytesCurrent": 3121312 - }, - { - "name": "_GrowableList", - "instancesCurrent": 926, - "bytesCurrent": 29632 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14952928, - "heapCapacity": 26591232, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292452", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 808, - "bytesCurrent": 29072 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40388, - "bytesCurrent": 3140416 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1228, - "bytesCurrent": 39296 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 2505, - "counts": { - "_List": 681, - "_Record": 1506, - "_GrowableList": 315, - "_Map": 3 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 1200 - }, - { - "function": "_allocateData", - "samples": 642 - }, - { - "function": "_List.of", - "samples": 303 - }, - { - "function": "current", - "samples": 300 - }, - { - "function": "_init", - "samples": 20 - }, - { - "function": "_sendInternal", - "samples": 18 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 6 - }, - { - "function": "_sliceInternal", - "samples": 3 - }, - { - "function": "_slice", - "samples": 3 - }, - { - "function": "Map._fromLiteral", - "samples": 3 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367475857524, - "endMicros": 1367475862890, - "retainedRootRows": 100, - "rssBefore": 355336192, - "rssAfter": 360349696, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14827424, - "heapCapacity": 26591232, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292485", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 408, - "bytesCurrent": 14672 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40084, - "bytesCurrent": 3120768 - }, - { - "name": "_GrowableList", - "instancesCurrent": 924, - "bytesCurrent": 29568 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14954368, - "heapCapacity": 20299776, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292701", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 808, - "bytesCurrent": 29072 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40389, - "bytesCurrent": 3140448 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1228, - "bytesCurrent": 39296 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 6819, - "counts": { - "_List": 3267, - "_SelectionPlan": 12, - "_GrowableList": 1953, - "_Map": 60, - "_Record": 1527 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 2100 - }, - { - "function": "_allocateData", - "samples": 2088 - }, - { - "function": "parameter", - "samples": 636 - }, - { - "function": "_List", - "samples": 355 - }, - { - "function": "_List.of", - "samples": 309 - }, - { - "function": "load", - "samples": 303 - }, - { - "function": "add", - "samples": 300 - }, - { - "function": "quote", - "samples": 180 - }, - { - "function": "_write", - "samples": 135 - }, - { - "function": "_addPart", - "samples": 96 - }, - { - "function": "writeSql", - "samples": 81 - }, - { - "function": "_compile", - "samples": 48 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 32, - 35, - 39, - 56, - 37, - 30, - 35, - 35, - 36, - 34, - 38, - 35, - 35, - 65, - 35, - 35, - 36, - 72, - 41, - 35, - 31, - 36, - 45, - 37, - 38, - 35, - 35, - 34, - 37, - 35, - 35, - 35, - 35, - 37, - 36, - 35, - 36, - 36, - 40, - 34, - 36, - 35, - 38, - 34, - 36, - 34, - 37, - 35, - 36, - 35, - 37, - 34, - 36, - 36, - 35, - 34, - 34, - 31, - 39, - 35, - 37, - 35, - 35, - 35, - 34, - 34, - 38, - 36, - 34, - 34, - 35, - 46, - 29, - 35, - 37, - 37, - 39, - 44, - 37, - 35, - 36, - 35, - 36, - 35, - 35, - 35, - 37, - 37, - 37, - 46, - 48, - 31, - 36, - 47, - 37, - 32, - 36, - 32, - 30, - 37, - 35, - 31, - 46, - 32, - 59, - 37, - 39, - 40, - 36, - 156, - 34, - 35, - 35, - 34, - 35, - 32, - 38, - 38, - 29, - 35, - 39, - 34, - 36, - 48, - 38, - 35, - 42, - 35, - 35, - 35, - 38, - 36, - 37, - 35, - 36, - 35, - 38, - 35, - 38, - 35, - 42, - 37, - 36, - 36, - 36, - 36, - 36, - 36, - 36, - 36, - 37, - 36, - 36, - 36, - 44, - 37, - 36, - 36, - 36, - 37, - 37, - 63, - 45, - 37, - 37, - 35, - 34, - 41, - 36, - 35, - 44, - 35, - 35, - 41, - 38, - 34, - 35, - 35, - 40, - 34, - 37, - 35, - 40, - 45, - 36, - 35, - 38, - 32, - 37, - 33, - 36, - 31, - 37, - 47, - 47, - 40, - 44, - 35, - 41, - 38 - ], - "p50Micros": 36, - "p95Micros": 47, - "minMicros": 29, - "maxMicros": 156, - "wallMicros": 7555, - "operationsPerSecond": 26472.534745201854 - }, - "orm": { - "samplesMicros": [ - 48, - 40, - 39, - 52, - 55, - 48, - 41, - 39, - 42, - 43, - 41, - 39, - 51, - 45, - 48, - 44, - 40, - 43, - 56, - 44, - 41, - 40, - 53, - 40, - 50, - 39, - 39, - 39, - 41, - 42, - 40, - 39, - 39, - 39, - 40, - 39, - 39, - 39, - 39, - 39, - 40, - 39, - 41, - 40, - 40, - 39, - 40, - 39, - 39, - 39, - 40, - 38, - 40, - 39, - 40, - 39, - 40, - 48, - 45, - 77, - 45, - 44, - 39, - 38, - 40, - 38, - 40, - 39, - 46, - 38, - 40, - 38, - 40, - 38, - 41, - 39, - 45, - 40, - 49, - 40, - 40, - 39, - 40, - 39, - 40, - 39, - 47, - 45, - 45, - 47, - 48, - 38, - 58, - 46, - 55, - 49, - 48, - 43, - 47, - 44, - 53, - 46, - 47, - 53, - 59, - 53, - 50, - 46, - 50, - 46, - 52, - 40, - 39, - 39, - 41, - 47, - 43, - 46, - 39, - 39, - 45, - 44, - 39, - 38, - 61, - 41, - 41, - 39, - 39, - 38, - 41, - 39, - 40, - 38, - 39, - 38, - 39, - 39, - 39, - 38, - 47, - 41, - 41, - 40, - 41, - 41, - 41, - 41, - 41, - 40, - 41, - 41, - 40, - 40, - 58, - 43, - 41, - 41, - 51, - 42, - 46, - 43, - 51, - 48, - 45, - 40, - 40, - 39, - 41, - 39, - 46, - 40, - 43, - 39, - 40, - 39, - 39, - 38, - 45, - 42, - 40, - 39, - 47, - 71, - 47, - 39, - 42, - 46, - 44, - 39, - 38, - 39, - 48, - 41, - 43, - 39, - 45, - 42, - 55, - 50 - ], - "p50Micros": 41, - "p95Micros": 53, - "minMicros": 38, - "maxMicros": 77, - "wallMicros": 8602, - "operationsPerSecond": 23250.40688212044 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 56, - 98, - 134, - 186, - 229, - 301, - 341, - 379, - 373, - 364, - 363, - 343, - 353, - 308, - 315, - 313, - 302, - 304, - 308, - 311, - 296, - 298, - 296, - 296, - 293, - 295, - 294, - 295, - 290, - 291, - 290, - 291, - 294, - 293, - 293, - 294, - 298, - 298, - 298, - 299, - 298, - 298, - 295, - 294, - 299, - 300, - 307, - 305, - 305, - 304, - 307, - 307, - 303, - 301, - 295, - 296, - 297, - 297, - 297, - 298, - 297, - 297, - 297, - 296, - 296, - 296, - 296, - 294, - 294, - 295, - 295, - 294, - 296, - 297, - 297, - 298, - 300, - 301, - 301, - 301, - 299, - 299, - 299, - 298, - 296, - 294, - 294, - 294, - 294, - 294, - 302, - 319, - 317, - 318, - 318, - 316, - 317, - 317, - 309, - 292, - 295, - 294, - 294, - 296, - 295, - 293, - 294, - 293, - 293, - 293, - 293, - 293, - 293, - 295, - 298, - 298, - 292, - 292, - 293, - 309, - 309, - 310, - 306, - 306, - 313, - 313, - 325, - 314, - 333, - 354, - 350, - 365, - 362, - 364, - 354, - 346, - 329, - 307, - 310, - 297, - 300, - 294, - 293, - 301, - 338, - 385, - 424, - 475, - 514, - 547, - 554, - 585, - 547, - 498, - 504, - 481, - 448, - 477, - 471, - 478, - 502, - 514, - 468, - 465, - 519, - 479, - 486, - 456, - 445, - 437, - 435, - 407, - 343, - 324, - 309, - 294, - 283, - 280, - 277, - 280, - 278, - 277, - 276, - 278, - 290, - 289, - 294, - 295, - 301, - 304, - 302, - 296, - 286, - 283, - 279, - 285, - 284, - 281, - 295, - 301 - ], - "p50Micros": 298, - "p95Micros": 486, - "minMicros": 56, - "maxMicros": 585, - "wallMicros": 8253, - "operationsPerSecond": 24233.61201987156 - }, - "orm": { - "samplesMicros": [ - 96, - 147, - 191, - 230, - 272, - 308, - 349, - 387, - 338, - 338, - 337, - 335, - 334, - 344, - 347, - 351, - 351, - 335, - 330, - 335, - 333, - 327, - 320, - 316, - 316, - 315, - 314, - 315, - 315, - 314, - 314, - 314, - 314, - 323, - 326, - 331, - 337, - 346, - 354, - 361, - 382, - 383, - 428, - 427, - 425, - 420, - 411, - 403, - 383, - 373, - 329, - 320, - 315, - 311, - 311, - 311, - 311, - 310, - 307, - 308, - 307, - 307, - 306, - 306, - 307, - 313, - 316, - 318, - 320, - 322, - 324, - 326, - 328, - 324, - 322, - 323, - 322, - 323, - 324, - 324, - 324, - 325, - 326, - 327, - 329, - 328, - 328, - 328, - 332, - 332, - 332, - 336, - 342, - 344, - 348, - 375, - 383, - 391, - 398, - 400, - 400, - 401, - 398, - 371, - 366, - 367, - 363, - 363, - 361, - 362, - 359, - 364, - 359, - 348, - 342, - 338, - 335, - 337, - 340, - 334, - 334, - 338, - 340, - 338, - 338, - 333, - 435, - 444, - 445, - 447, - 447, - 448, - 452, - 455, - 354, - 345, - 344, - 345, - 345, - 346, - 341, - 340, - 340, - 340, - 344, - 347, - 347, - 345, - 344, - 341, - 339, - 341, - 336, - 334, - 366, - 417, - 424, - 427, - 429, - 478, - 534, - 539, - 504, - 449, - 438, - 431, - 426, - 385, - 329, - 322, - 321, - 321, - 321, - 323, - 326, - 325, - 323, - 317, - 321, - 326, - 326, - 324, - 352, - 342, - 343, - 346, - 347, - 349, - 352, - 359, - 338, - 341, - 341, - 341, - 338, - 334, - 333, - 328, - 321, - 307 - ], - "p50Micros": 338, - "p95Micros": 445, - "minMicros": 96, - "maxMicros": 539, - "wallMicros": 8851, - "operationsPerSecond": 22596.316800361543 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 12, - 62, - 101, - 150, - 187, - 227, - 266, - 302, - 281, - 282, - 268, - 271, - 269, - 269, - 270, - 266, - 266, - 264, - 265, - 268, - 272, - 276, - 275, - 277, - 280, - 280, - 278, - 273, - 268, - 268, - 267, - 267, - 267, - 269, - 269, - 270, - 270, - 270, - 268, - 267, - 265, - 264, - 264, - 263, - 263, - 263, - 264, - 260, - 253, - 246, - 239, - 232, - 225, - 218, - 217, - 216, - 233, - 242, - 249, - 256, - 262, - 268, - 275, - 262 - ], - "p50Micros": 266, - "p95Micros": 280, - "minMicros": 12, - "maxMicros": 302, - "wallMicros": 15976 - }, - "sqlMicros": [ - 47, - 38, - 46, - 35, - 38, - 37, - 34, - 42, - 37, - 36, - 36, - 36, - 36, - 36, - 37, - 36, - 34, - 37, - 39, - 41, - 41, - 36, - 38, - 37, - 38, - 36, - 36, - 36, - 36, - 36, - 37, - 38, - 38, - 37, - 36, - 36, - 37, - 36, - 36, - 36, - 36, - 36, - 36, - 36, - 36, - 37, - 32, - 30, - 30, - 30, - 29, - 30, - 30, - 30, - 29, - 45, - 38, - 36, - 36, - 36, - 36, - 36, - 36, - 36 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 8, - 61, - 102, - 143, - 184, - 224, - 264, - 304, - 276, - 276, - 275, - 274, - 274, - 275, - 279, - 279, - 280, - 280, - 286, - 287, - 288, - 284, - 283, - 283, - 283, - 277, - 279, - 279, - 283, - 288, - 291, - 296, - 300, - 296, - 295, - 290, - 288, - 285, - 283, - 288, - 289, - 290, - 290, - 289, - 288, - 279, - 276, - 276, - 276, - 276, - 275, - 275, - 277, - 278, - 278, - 278, - 278, - 278, - 277, - 271, - 266, - 261, - 256, - 255 - ], - "p50Micros": 278, - "p95Micros": 296, - "minMicros": 8, - "maxMicros": 304, - "wallMicros": 17002 - }, - "sqlMicros": [ - 38, - 31, - 31, - 32, - 31, - 31, - 31, - 32, - 31, - 31, - 31, - 31, - 31, - 36, - 32, - 32, - 32, - 37, - 33, - 33, - 32, - 31, - 32, - 31, - 31, - 35, - 32, - 36, - 37, - 36, - 36, - 35, - 31, - 31, - 31, - 33, - 33, - 33, - 34, - 31, - 31, - 32, - 32, - 32, - 32, - 31, - 31, - 31, - 31, - 32, - 32, - 33, - 31, - 32, - 32, - 31, - 31, - 32, - 29, - 28, - 28, - 28, - 31, - 32 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 8 - ], - "sqlMicros": [ - 56 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 6 - ], - "sqlMicros": [ - 45 - ], - "ormDecodeMicros": [ - 5 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367476108419, - "endMicros": 1367476108861, - "retainedRootRows": 10, - "rssBefore": 370704384, - "rssAfter": 370802688, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14736176, - "heapCapacity": 20824064, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292737", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 18, - "bytesCurrent": 592 - }, - { - "name": "_Map", - "instancesCurrent": 13, - "bytesCurrent": 832 - }, - { - "name": "_List", - "instancesCurrent": 40099, - "bytesCurrent": 3121264 - }, - { - "name": "_GrowableList", - "instancesCurrent": 926, - "bytesCurrent": 29632 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14736592, - "heapCapacity": 20824064, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292891", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 28, - "bytesCurrent": 912 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40107, - "bytesCurrent": 3121520 - }, - { - "name": "_GrowableList", - "instancesCurrent": 938, - "bytesCurrent": 30016 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 62, - "counts": { - "_List": 23, - "_Record": 33, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 30 - }, - { - "function": "_allocateData", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_init", - "samples": 1 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367476288958, - "endMicros": 1367476289760, - "retainedRootRows": 10, - "rssBefore": 378912768, - "rssAfter": 379125760, - "processLifetimeMaxRss": 486506496 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14734864, - "heapCapacity": 20824064, - "externalUsage": 80 - }, - "gcTimestamp": "1789456292920", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 18, - "bytesCurrent": 592 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40093, - "bytesCurrent": 3120656 - }, - { - "name": "_GrowableList", - "instancesCurrent": 924, - "bytesCurrent": 29568 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14736752, - "heapCapacity": 20824064, - "externalUsage": 80 - }, - "gcTimestamp": "1789456293069", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 28, - "bytesCurrent": 912 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40107, - "bytesCurrent": 3121520 - }, - { - "name": "_GrowableList", - "instancesCurrent": 938, - "bytesCurrent": 30016 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 371, - "counts": { - "_List": 188, - "_SelectionPlan": 3, - "_GrowableList": 120, - "_Map": 18, - "_Record": 42 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 123 - }, - { - "function": "quote", - "samples": 42 - }, - { - "function": "_write", - "samples": 30 - }, - { - "function": "", - "samples": 30 - }, - { - "function": "_addPart", - "samples": 24 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "_List", - "samples": 15 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_init", - "samples": 10 - }, - { - "function": "_sendInternal", - "samples": 9 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - } - ] - }, - { - "scenario": "postgres_loopback", - "environment": { - "backend": "postgres_loopback", - "databaseVersion": "18.4 (Homebrew)", - "pid": 5140 - }, - "oneWayDelayMs": 0, - "echoRoundTripMicros": { - "samplesMicros": [ - 175, - 164, - 193, - 159, - 165, - 161, - 393, - 169, - 185, - 175 - ], - "p50Micros": 169, - "p95Micros": 393, - "minMicros": 159, - "maxMicros": 393, - "wallMicros": 1939 - }, - "selectOneMicros": { - "samplesMicros": [ - 1188, - 816, - 716, - 712, - 723, - 674, - 746, - 735, - 888, - 840 - ], - "p50Micros": 735, - "p95Micros": 1188, - "minMicros": 674, - "maxMicros": 1188, - "wallMicros": 8038, - "operationsPerSecond": 1244.0905697934809 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 1038, - 2245, - 1032, - 1238, - 1019, - 1079, - 1000, - 1021, - 1149, - 991, - 1035, - 1133, - 1114, - 947, - 812, - 868, - 904, - 928, - 923, - 952, - 970, - 1269, - 978, - 1015, - 1215, - 1001, - 937, - 1420, - 1234, - 949, - 928, - 986, - 1153, - 905, - 892, - 1220, - 1039, - 920, - 1001, - 1024, - 1114, - 1606, - 1049, - 949, - 1231, - 893, - 864, - 1203, - 992, - 856, - 890, - 933, - 1097, - 873, - 841, - 973, - 1225, - 888, - 832, - 1033, - 939, - 928, - 931, - 959, - 1105, - 935, - 874, - 901, - 954, - 960, - 937, - 868, - 861, - 975, - 961, - 884, - 844, - 1168, - 990, - 950, - 1007, - 1032, - 923, - 857, - 868, - 1099, - 1018, - 854, - 958, - 893, - 1212, - 888, - 882, - 912, - 939, - 1012, - 830, - 849, - 881, - 969, - 861, - 877, - 899, - 925, - 905, - 799, - 1021, - 1012, - 926, - 985, - 944, - 987, - 944, - 874, - 799, - 874, - 1011, - 877, - 818, - 854, - 952, - 885, - 869, - 807, - 902, - 899, - 832, - 843, - 1065, - 880, - 899, - 824, - 796, - 816, - 815, - 884, - 896, - 815, - 830, - 1030, - 902, - 822, - 872, - 1388, - 1066, - 962, - 956, - 914, - 861, - 804, - 833, - 881, - 917, - 871, - 798, - 885, - 866, - 878, - 838, - 800, - 950, - 958, - 857, - 838, - 805, - 894, - 1060, - 856, - 770, - 880, - 923, - 861, - 834, - 819, - 868, - 938, - 1110, - 813, - 903, - 1047, - 898, - 786, - 907, - 829, - 922, - 934, - 851, - 862, - 854, - 891, - 933, - 799, - 823, - 883, - 846, - 772, - 799, - 885, - 887, - 858 - ], - "p50Micros": 907, - "p95Micros": 1215, - "minMicros": 770, - "maxMicros": 2245, - "wallMicros": 189681, - "operationsPerSecond": 1054.401864182496 - }, - "orm": { - "samplesMicros": [ - 1168, - 1288, - 1092, - 1064, - 1202, - 1366, - 1064, - 1048, - 1151, - 1139, - 1092, - 1018, - 1098, - 955, - 871, - 856, - 1015, - 1128, - 985, - 965, - 1096, - 1055, - 1023, - 1032, - 1225, - 1432, - 1008, - 1228, - 1186, - 1072, - 1097, - 1215, - 1118, - 1014, - 1041, - 1072, - 1094, - 1085, - 973, - 983, - 1052, - 986, - 1390, - 1074, - 1116, - 1072, - 980, - 992, - 1083, - 944, - 940, - 945, - 1034, - 967, - 892, - 1026, - 1189, - 985, - 935, - 940, - 1138, - 974, - 971, - 1008, - 1043, - 1118, - 985, - 914, - 1079, - 1149, - 995, - 899, - 1008, - 1087, - 959, - 920, - 979, - 961, - 1077, - 991, - 1034, - 1136, - 997, - 964, - 978, - 1004, - 1082, - 951, - 1015, - 1021, - 1117, - 1009, - 929, - 920, - 1145, - 1103, - 873, - 888, - 1406, - 1183, - 900, - 882, - 1035, - 1039, - 948, - 875, - 967, - 956, - 1010, - 972, - 967, - 951, - 933, - 1013, - 898, - 943, - 1052, - 945, - 881, - 860, - 1010, - 941, - 898, - 911, - 891, - 981, - 857, - 852, - 938, - 851, - 893, - 926, - 852, - 815, - 1118, - 1050, - 885, - 843, - 988, - 987, - 1028, - 922, - 1023, - 961, - 1055, - 995, - 971, - 889, - 1071, - 1043, - 872, - 975, - 1051, - 1342, - 878, - 860, - 1163, - 983, - 842, - 857, - 942, - 966, - 937, - 873, - 913, - 968, - 948, - 891, - 836, - 854, - 1009, - 975, - 868, - 857, - 1003, - 942, - 936, - 896, - 979, - 938, - 966, - 924, - 869, - 936, - 1060, - 927, - 927, - 914, - 965, - 995, - 863, - 824, - 934, - 996, - 905, - 885, - 878, - 908, - 1032, - 965 - ], - "p50Micros": 979, - "p95Micros": 1189, - "minMicros": 815, - "maxMicros": 1432, - "wallMicros": 199665, - "operationsPerSecond": 1001.6778103323066 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 5307, - 6868, - 6951, - 7029, - 7121, - 8799, - 8859, - 8887, - 3657, - 3840, - 3824, - 3778, - 3714, - 4131, - 4160, - 4268, - 4365, - 4305, - 4319, - 4337, - 4345, - 4070, - 3967, - 3945, - 3928, - 3996, - 3995, - 3986, - 3987, - 4029, - 4077, - 4071, - 4183, - 4409, - 4470, - 4472, - 4489, - 4606, - 4635, - 4560, - 4689, - 4574, - 4534, - 4541, - 4534, - 4440, - 4375, - 4344, - 4327, - 4020, - 4171, - 4079, - 4063, - 4054, - 4053, - 4051, - 4050, - 3910, - 3910, - 3907, - 3916, - 4065, - 4075, - 4037, - 4128, - 4209, - 4214, - 4219, - 4228, - 4143, - 4088, - 4239, - 4182, - 4162, - 4153, - 4160, - 4155, - 4279, - 4144, - 4328, - 4273, - 4451, - 4456, - 4449, - 4446, - 4282, - 4293, - 4300, - 4313, - 4146, - 4134, - 4081, - 4174, - 4149, - 4157, - 4151, - 4138, - 4312, - 4327, - 4330, - 4325, - 4097, - 4070, - 4073, - 4075, - 4103, - 4109, - 4108, - 4115, - 4365, - 4382, - 4383, - 4413, - 4238, - 4228, - 4228, - 4220, - 4237, - 4226, - 4229, - 4197, - 4009, - 4013, - 4008, - 4014, - 4005, - 3961, - 3971, - 4118, - 4188, - 4172, - 4173, - 4174, - 3939, - 3923, - 3867, - 3962, - 3914, - 3923, - 3928, - 3929, - 4016, - 4040, - 4110, - 4121, - 4282, - 4292, - 4447, - 4335, - 4322, - 4305, - 4236, - 4224, - 4007, - 4015, - 4024, - 4031, - 4298, - 4320, - 4323, - 4531, - 4527, - 4678, - 4609, - 4604, - 4601, - 4619, - 4617, - 4615, - 4362, - 4356, - 4306, - 4401, - 4253, - 4188, - 4286, - 4233, - 4302, - 4289, - 4237, - 4334, - 4137, - 4135, - 4132, - 4136, - 4145, - 4147, - 4101, - 4197, - 4207, - 4128, - 4228, - 4330, - 4213, - 4322, - 4252, - 4241, - 3653, - 3596, - 3577 - ], - "p50Micros": 4197, - "p95Micros": 4635, - "minMicros": 3577, - "maxMicros": 8887, - "wallMicros": 109456, - "operationsPerSecond": 1827.2182429469376 - }, - "orm": { - "samplesMicros": [ - 2424, - 2526, - 2623, - 2719, - 4735, - 4900, - 5018, - 5114, - 4632, - 4614, - 4595, - 4575, - 4393, - 4345, - 4198, - 4365, - 4245, - 4290, - 4306, - 4314, - 4526, - 4398, - 4587, - 4760, - 4647, - 4694, - 4635, - 4887, - 5282, - 5281, - 5261, - 5296, - 6022, - 5696, - 6105, - 6156, - 5200, - 5672, - 5617, - 5599, - 4822, - 4768, - 4735, - 4626, - 4506, - 4565, - 4513, - 4683, - 4856, - 4710, - 4956, - 4967, - 4700, - 4976, - 4793, - 4788, - 4627, - 4706, - 5055, - 4860, - 4998, - 5248, - 5058, - 5232, - 4593, - 4614, - 4617, - 5133, - 4449, - 4737, - 4609, - 4608, - 4620, - 4379, - 4570, - 4761, - 4585, - 4875, - 4719, - 4720, - 4868, - 5133, - 4917, - 5076, - 4799, - 4816, - 4820, - 4810, - 4883, - 5244, - 5124, - 5145, - 4857, - 5130, - 4869, - 5022, - 4352, - 4690, - 4438, - 4446, - 4746, - 4632, - 4887, - 5067, - 4619, - 4827, - 4719, - 4710, - 4351, - 4315, - 4302, - 4177, - 4674, - 4734, - 4664, - 4923, - 4902, - 5200, - 5108, - 5060, - 4442, - 4442, - 4783, - 4430, - 4407, - 4345, - 4243, - 4465, - 4805, - 4742, - 4914, - 4846, - 6295, - 6907, - 6821, - 6820, - 6865, - 6782, - 6779, - 7049, - 5053, - 4672, - 4637, - 4585, - 4184, - 4157, - 4182, - 4173, - 4115, - 4134, - 4142, - 4142, - 4421, - 4507, - 4827, - 4640, - 4917, - 4942, - 5350, - 5223, - 4849, - 4751, - 4718, - 4689, - 4463, - 4447, - 4342, - 4238, - 4374, - 4444, - 4390, - 4566, - 4453, - 4441, - 4453, - 4453, - 4524, - 4459, - 4444, - 4455, - 4380, - 4442, - 4471, - 4569, - 4520, - 4740, - 4641, - 4615, - 4437, - 4376, - 4330, - 4227, - 3897, - 3949, - 3960, - 3962, - 4463, - 4418, - 4374, - 4341 - ], - "p50Micros": 4647, - "p95Micros": 6022, - "minMicros": 2424, - "maxMicros": 7049, - "wallMicros": 119678, - "operationsPerSecond": 1671.1509216397333 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 113, - 117, - 117, - 119, - 2059, - 2118, - 2175, - 2228, - 2302, - 2283, - 2265, - 2267, - 2039, - 2060, - 2070, - 2076, - 2721, - 2728, - 2723, - 2730, - 2036, - 2010, - 2011, - 2007, - 2778, - 2775, - 2768, - 2771, - 3294, - 3274, - 3276, - 3283, - 2114, - 2100, - 2095, - 2094, - 4477, - 4488, - 4540, - 4602, - 2748, - 2724, - 2723, - 2639, - 2241, - 2131, - 2150, - 2141, - 2125, - 2170, - 2182, - 2214, - 2438, - 2388, - 2351, - 2345, - 2388, - 2343, - 2327, - 2303, - 2014, - 2010, - 2011, - 2008 - ], - "p50Micros": 2241, - "p95Micros": 4477, - "minMicros": 113, - "maxMicros": 4602, - "wallMicros": 151217 - }, - "sqlMicros": [ - 1903, - 1991, - 2074, - 2099, - 2244, - 2242, - 2228, - 2222, - 1963, - 1959, - 2072, - 2031, - 2569, - 2615, - 2819, - 2683, - 1875, - 2048, - 1963, - 1956, - 2698, - 2718, - 2723, - 2724, - 3141, - 3308, - 3238, - 3237, - 1910, - 2118, - 2108, - 2046, - 4378, - 4444, - 4455, - 4534, - 2371, - 2666, - 2514, - 2898, - 2107, - 2020, - 2164, - 2096, - 2028, - 2080, - 2103, - 2138, - 2214, - 2236, - 2524, - 2289, - 2238, - 2383, - 2290, - 2255, - 1945, - 1970, - 1976, - 1970, - 2425, - 2404, - 2384, - 2378 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 106, - 573, - 599, - 615, - 3002, - 3138, - 3221, - 3350, - 2359, - 2293, - 2294, - 2244, - 2104, - 2110, - 2107, - 2091, - 2271, - 2271, - 2254, - 2253, - 2302, - 2261, - 2268, - 2281, - 1980, - 1970, - 1956, - 1935, - 2051, - 2050, - 2100, - 2138, - 2267, - 2299, - 2290, - 2268, - 2115, - 2067, - 2073, - 2063, - 1972, - 1967, - 1933, - 1920, - 2088, - 2091, - 2078, - 2092, - 2062, - 2333, - 2364, - 2443, - 2677, - 2351, - 2319, - 2240, - 2011, - 2003, - 2003, - 2049, - 1989, - 1984, - 1981, - 1940 - ], - "p50Micros": 2100, - "p95Micros": 3002, - "minMicros": 106, - "maxMicros": 3350, - "wallMicros": 134879 - }, - "sqlMicros": [ - 2313, - 2513, - 2665, - 2659, - 2110, - 2329, - 2217, - 2174, - 2003, - 2034, - 2033, - 2028, - 2157, - 2193, - 2192, - 2189, - 2106, - 2286, - 2207, - 2215, - 1896, - 1902, - 1889, - 1873, - 1966, - 1991, - 2000, - 2066, - 2158, - 2223, - 2202, - 2191, - 2004, - 1997, - 2009, - 2000, - 1892, - 1897, - 1875, - 1860, - 2009, - 2022, - 2021, - 2035, - 1880, - 2256, - 2306, - 2378, - 2190, - 2401, - 2257, - 2173, - 1928, - 1941, - 1942, - 1956, - 1913, - 1930, - 1924, - 1877, - 2127, - 2105, - 2079, - 2076 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 38 - ], - "sqlMicros": [ - 898 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 32 - ], - "sqlMicros": [ - 896 - ], - "ormDecodeMicros": [ - 12 - ], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367478259746, - "endMicros": 1367478264702, - "retainedRootRows": 100, - "rssBefore": 304562176, - "rssAfter": 305807360, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16899520, - "heapCapacity": 36765696, - "externalUsage": 272 - }, - "gcTimestamp": "1789456294883", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 4992 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 46791, - "bytesCurrent": 3592272 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16956176, - "heapCapacity": 22921216, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295099", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 9792 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46815, - "bytesCurrent": 3595696 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 3694, - "counts": { - "_List": 2389, - "_GrowableList": 993, - "_Record": 312 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1983 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "_List", - "samples": 114 - }, - { - "function": "current", - "samples": 48 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 19 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_worker", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - }, - { - "function": "bindParameters", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367478508292, - "endMicros": 1367478512415, - "retainedRootRows": 100, - "rssBefore": 329531392, - "rssAfter": 330874880, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16910128, - "heapCapacity": 22921216, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295134", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 4992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46814, - "bytesCurrent": 3594656 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16962624, - "heapCapacity": 26591232, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295355", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 9792 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46821, - "bytesCurrent": 3596672 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 4034, - "counts": { - "_List": 2561, - "_GrowableList": 1134, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 318 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 2139 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "_List", - "samples": 139 - }, - { - "function": "current", - "samples": 48 - }, - { - "function": "quote", - "samples": 48 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "_write", - "samples": 33 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 24 - }, - { - "function": "writeSql", - "samples": 21 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 861, - 799, - 801, - 688, - 690, - 836, - 832, - 814, - 680, - 728, - 775, - 788, - 719, - 669, - 668, - 751, - 723, - 858, - 741, - 678, - 573, - 567, - 577, - 723, - 680, - 578, - 574, - 572, - 625, - 811, - 700, - 554, - 534, - 668, - 569, - 668, - 687, - 594, - 582, - 542, - 542, - 547, - 618, - 614, - 547, - 547, - 547, - 580, - 586, - 644, - 618, - 558, - 527, - 522, - 600, - 597, - 749, - 564, - 577, - 523, - 513, - 603, - 556, - 726, - 596, - 537, - 537, - 533, - 577, - 709, - 666, - 519, - 549, - 532, - 519, - 583, - 532, - 558, - 519, - 817, - 564, - 539, - 536, - 596, - 602, - 543, - 529, - 570, - 536, - 506, - 629, - 558, - 530, - 513, - 538, - 538, - 542, - 755, - 701, - 552, - 503, - 497, - 515, - 537, - 573, - 671, - 550, - 526, - 501, - 525, - 569, - 763, - 661, - 497, - 541, - 607, - 533, - 497, - 577, - 554, - 597, - 516, - 539, - 540, - 527, - 602, - 757, - 532, - 514, - 506, - 514, - 553, - 522, - 616, - 595, - 541, - 531, - 523, - 553, - 523, - 648, - 665, - 583, - 523, - 602, - 531, - 556, - 590, - 669, - 499, - 515, - 498, - 568, - 543, - 544, - 644, - 586, - 537, - 500, - 495, - 504, - 522, - 569, - 517, - 572, - 493, - 508, - 551, - 522, - 641, - 600, - 549, - 541, - 484, - 503, - 512, - 524, - 806, - 615, - 502, - 521, - 488, - 507, - 517, - 552, - 531, - 529, - 521, - 583, - 485, - 490, - 509, - 708, - 534, - 507, - 538, - 516, - 517, - 532, - 676 - ], - "p50Micros": 553, - "p95Micros": 788, - "minMicros": 484, - "maxMicros": 861, - "wallMicros": 117532, - "operationsPerSecond": 1701.6642276146072 - }, - "orm": { - "samplesMicros": [ - 974, - 910, - 742, - 739, - 838, - 861, - 827, - 779, - 797, - 699, - 799, - 848, - 819, - 728, - 699, - 771, - 824, - 884, - 764, - 725, - 565, - 565, - 677, - 926, - 656, - 593, - 572, - 572, - 618, - 703, - 643, - 584, - 587, - 615, - 569, - 585, - 712, - 725, - 642, - 613, - 618, - 639, - 809, - 623, - 590, - 592, - 607, - 633, - 704, - 833, - 691, - 581, - 562, - 554, - 604, - 595, - 797, - 638, - 546, - 621, - 549, - 592, - 668, - 718, - 645, - 580, - 580, - 568, - 591, - 572, - 702, - 599, - 556, - 529, - 616, - 598, - 639, - 740, - 574, - 658, - 963, - 616, - 595, - 735, - 662, - 657, - 543, - 554, - 577, - 643, - 869, - 776, - 645, - 588, - 566, - 658, - 563, - 628, - 591, - 550, - 620, - 544, - 582, - 581, - 696, - 701, - 582, - 590, - 527, - 595, - 613, - 615, - 603, - 554, - 563, - 550, - 625, - 553, - 661, - 612, - 564, - 618, - 556, - 608, - 546, - 559, - 674, - 580, - 545, - 525, - 546, - 623, - 566, - 614, - 580, - 565, - 600, - 574, - 575, - 565, - 645, - 632, - 588, - 559, - 549, - 539, - 599, - 560, - 591, - 534, - 564, - 544, - 540, - 548, - 588, - 689, - 591, - 615, - 540, - 515, - 537, - 562, - 760, - 603, - 588, - 545, - 544, - 544, - 571, - 561, - 675, - 597, - 624, - 551, - 527, - 542, - 564, - 615, - 577, - 521, - 512, - 524, - 544, - 633, - 634, - 608, - 531, - 548, - 526, - 531, - 553, - 557, - 771, - 776, - 579, - 570, - 530, - 537, - 580, - 729 - ], - "p50Micros": 593, - "p95Micros": 827, - "minMicros": 512, - "maxMicros": 974, - "wallMicros": 125300, - "operationsPerSecond": 1596.1691939345571 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 1472, - 1523, - 1606, - 1669, - 2883, - 2940, - 2972, - 3017, - 2640, - 2651, - 2670, - 2645, - 2534, - 2682, - 2614, - 2603, - 2978, - 2952, - 2897, - 2892, - 3102, - 3099, - 3218, - 3216, - 3238, - 3362, - 3300, - 3309, - 3212, - 3226, - 3228, - 3215, - 3144, - 3149, - 3149, - 3195, - 3659, - 3853, - 3688, - 3653, - 3602, - 3731, - 3586, - 3735, - 3641, - 3642, - 3658, - 3661, - 3524, - 3449, - 3553, - 3473, - 3452, - 3569, - 3497, - 3507, - 3676, - 3713, - 3676, - 3775, - 2983, - 2984, - 3028, - 3040, - 3633, - 3615, - 3562, - 3668, - 3745, - 3763, - 3731, - 3723, - 3746, - 3924, - 3925, - 3899, - 3950, - 3885, - 3985, - 4000, - 3202, - 3139, - 3117, - 3311, - 4424, - 4429, - 4440, - 4359, - 4382, - 4379, - 4378, - 4368, - 4263, - 4203, - 4297, - 4234, - 4129, - 4136, - 4143, - 4971, - 5902, - 5989, - 6044, - 6155, - 6820, - 6840, - 5959, - 6882, - 3837, - 3749, - 3661, - 3629, - 3012, - 3155, - 3059, - 3057, - 2991, - 3000, - 3005, - 3008, - 3677, - 3702, - 3752, - 3767, - 4015, - 3979, - 4074, - 4028, - 3111, - 3092, - 2981, - 3079, - 2742, - 2720, - 2722, - 2713, - 2538, - 2538, - 2496, - 2571, - 2489, - 2486, - 2620, - 2520, - 2773, - 2805, - 2850, - 3037, - 2865, - 2886, - 2896, - 2899, - 2687, - 2805, - 2691, - 2678, - 2589, - 2709, - 2635, - 2632, - 2484, - 2494, - 2504, - 2507, - 2673, - 2668, - 2639, - 2715, - 2849, - 2860, - 2810, - 2893, - 3069, - 3084, - 3080, - 3078, - 3070, - 3194, - 3125, - 3121, - 2893, - 2901, - 2867, - 2945, - 2837, - 2983, - 2901, - 2983, - 2775, - 2908, - 2838, - 2830, - 3201, - 3205, - 3153, - 3237, - 3295, - 3383, - 3301, - 3298 - ], - "p50Micros": 3149, - "p95Micros": 4429, - "minMicros": 1472, - "maxMicros": 6882, - "wallMicros": 84664, - "operationsPerSecond": 2362.2791269016348 - }, - "orm": { - "samplesMicros": [ - 1375, - 1459, - 1521, - 1573, - 2824, - 2898, - 2947, - 2993, - 2932, - 2825, - 2944, - 2875, - 3112, - 3093, - 3122, - 3315, - 3278, - 3500, - 3499, - 3443, - 3210, - 3181, - 3155, - 3116, - 2928, - 2905, - 2893, - 2894, - 2836, - 2841, - 2861, - 2884, - 2955, - 2905, - 2947, - 3195, - 3241, - 3261, - 3410, - 3338, - 3340, - 3247, - 3420, - 3240, - 3078, - 3043, - 3035, - 3024, - 2861, - 2807, - 2939, - 2867, - 2970, - 2911, - 2837, - 2945, - 2958, - 2963, - 2978, - 2997, - 2863, - 2854, - 2863, - 2869, - 3005, - 3016, - 3009, - 2987, - 2988, - 3013, - 3013, - 3013, - 3039, - 2925, - 3042, - 2975, - 2959, - 2889, - 2896, - 3073, - 2853, - 2882, - 2891, - 2895, - 3351, - 3423, - 3441, - 3452, - 3331, - 3316, - 3321, - 3319, - 3030, - 2969, - 2953, - 2955, - 2979, - 2964, - 2963, - 2964, - 2908, - 3126, - 2968, - 3101, - 3206, - 3353, - 3268, - 3263, - 3175, - 3118, - 3039, - 3158, - 2850, - 2846, - 2806, - 2903, - 2666, - 2684, - 2685, - 2686, - 2668, - 2631, - 2774, - 2745, - 3179, - 3201, - 3195, - 3233, - 3252, - 3274, - 3262, - 3231, - 2971, - 3172, - 3072, - 3062, - 3064, - 3024, - 3027, - 3034, - 2884, - 2860, - 2779, - 2917, - 3065, - 3127, - 3071, - 3193, - 3165, - 3172, - 3359, - 3214, - 3059, - 2999, - 2986, - 2974, - 2935, - 2980, - 2987, - 2995, - 3290, - 3346, - 3367, - 3383, - 3940, - 3890, - 4021, - 3980, - 3657, - 3875, - 3660, - 3797, - 3086, - 3285, - 3079, - 3203, - 2940, - 2939, - 2929, - 2910, - 2726, - 2721, - 2718, - 2718, - 2772, - 2794, - 2803, - 2813, - 3060, - 3128, - 3151, - 3172, - 3343, - 3322, - 3259, - 3376, - 3164, - 3048, - 2913, - 3006 - ], - "p50Micros": 3013, - "p95Micros": 3452, - "minMicros": 1375, - "maxMicros": 4021, - "wallMicros": 76859, - "operationsPerSecond": 2602.167605615478 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 53, - 53, - 54, - 55, - 1188, - 1229, - 1274, - 1313, - 1428, - 1419, - 1408, - 1409, - 1401, - 1389, - 1387, - 1377, - 1591, - 1585, - 1586, - 1595, - 1307, - 1287, - 1293, - 1307, - 1339, - 1333, - 1341, - 1357, - 1461, - 1446, - 1432, - 1417, - 1405, - 1389, - 1381, - 1395, - 1683, - 1684, - 1687, - 1700, - 1518, - 1497, - 1494, - 1483, - 1299, - 1287, - 1282, - 1281, - 1371, - 1368, - 1363, - 1365, - 1328, - 1335, - 1343, - 1356, - 1323, - 1288, - 1270, - 1337, - 1372, - 1412, - 1441, - 1389 - ], - "p50Micros": 1371, - "p95Micros": 1683, - "minMicros": 53, - "maxMicros": 1700, - "wallMicros": 84240 - }, - "sqlMicros": [ - 1125, - 1169, - 1209, - 1248, - 1370, - 1390, - 1376, - 1376, - 1337, - 1365, - 1320, - 1393, - 1520, - 1528, - 1532, - 1627, - 1237, - 1257, - 1231, - 1309, - 1265, - 1284, - 1305, - 1306, - 1412, - 1413, - 1410, - 1384, - 1351, - 1356, - 1356, - 1352, - 1575, - 1704, - 1666, - 1662, - 1455, - 1462, - 1466, - 1453, - 1250, - 1247, - 1254, - 1253, - 1326, - 1335, - 1329, - 1329, - 1258, - 1261, - 1356, - 1321, - 1264, - 1205, - 1291, - 1223, - 1275, - 1326, - 1472, - 1339, - 1291, - 1250, - 1418, - 1247 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 68, - 108, - 120, - 128, - 1428, - 1484, - 1532, - 1578, - 1334, - 1335, - 1345, - 1354, - 1439, - 1440, - 1529, - 1564, - 1659, - 1668, - 1576, - 1549, - 1563, - 1548, - 1736, - 1771, - 1931, - 1900, - 1737, - 1670, - 1324, - 1320, - 1324, - 1370, - 1730, - 1736, - 1730, - 1736, - 1430, - 1394, - 1382, - 1377, - 1534, - 1604, - 1628, - 1715, - 1903, - 1803, - 1779, - 1713, - 1471, - 1463, - 1448, - 1452, - 1414, - 1398, - 1398, - 1459, - 1479, - 1466, - 1463, - 1412, - 1501, - 1502, - 1501, - 1499 - ], - "p50Micros": 1484, - "p95Micros": 1803, - "minMicros": 68, - "maxMicros": 1931, - "wallMicros": 92952 - }, - "sqlMicros": [ - 1286, - 1378, - 1401, - 1437, - 1258, - 1284, - 1301, - 1307, - 1364, - 1377, - 1389, - 1504, - 1560, - 1363, - 1507, - 1729, - 1454, - 1499, - 1634, - 1718, - 1834, - 1770, - 1642, - 1613, - 1177, - 1216, - 1389, - 1276, - 1649, - 1687, - 1693, - 1612, - 1331, - 1340, - 1345, - 1329, - 1357, - 1589, - 1533, - 1695, - 1656, - 1853, - 1599, - 1746, - 1387, - 1410, - 1404, - 1383, - 1269, - 1412, - 1300, - 1460, - 1342, - 1475, - 1417, - 1356, - 1441, - 1402, - 1514, - 1457, - 1557, - 1664, - 1571, - 1510 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 34 - ], - "sqlMicros": [ - 588 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 32 - ], - "sqlMicros": [ - 561 - ], - "ormDecodeMicros": [ - 7 - ], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367478798643, - "endMicros": 1367478801421, - "retainedRootRows": 100, - "rssBefore": 344834048, - "rssAfter": 345718784, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16914944, - "heapCapacity": 29212672, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295424", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 3392 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 46869, - "bytesCurrent": 3601888 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16922704, - "heapCapacity": 23445504, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295637", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 6592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46864, - "bytesCurrent": 3602320 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 2665, - "counts": { - "_List": 1372, - "_GrowableList": 987, - "_Record": 306 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1062 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "_List", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_List.filled", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - }, - { - "function": "bindParameters", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367479042645, - "endMicros": 1367479045697, - "retainedRootRows": 100, - "rssBefore": 357728256, - "rssAfter": 358711296, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16913632, - "heapCapacity": 23445504, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295670", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 3392 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46863, - "bytesCurrent": 3601280 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16922704, - "heapCapacity": 25542656, - "externalUsage": 272 - }, - "gcTimestamp": "1789456295883", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 6592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46864, - "bytesCurrent": 3602320 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 2944, - "counts": { - "_List": 1507, - "_GrowableList": 1104, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 312 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1185 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "_List", - "samples": 37 - }, - { - "function": "quote", - "samples": 36 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "_write", - "samples": 27 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "writeSql", - "samples": 15 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 2580, - 2216, - 2080, - 1982, - 2082, - 2048, - 2081, - 2100, - 1842, - 1830, - 1815, - 1866, - 1869, - 1909, - 2080, - 1826, - 1857, - 1916, - 1980, - 1991, - 1873, - 1874, - 2073, - 1856, - 1829, - 1696, - 1834, - 1788, - 1898, - 1727, - 1876, - 1724, - 1918, - 1763, - 1892, - 1773, - 1723, - 1825, - 1747, - 1781, - 1727, - 1888, - 1736, - 1814, - 1804, - 1801, - 1867, - 1753, - 1803, - 1733, - 1652, - 2066, - 1775, - 2064, - 1801, - 1965, - 1705, - 2064, - 1759, - 2096, - 1782, - 2116, - 1806, - 2397, - 1857, - 2083, - 1860, - 2193, - 1753, - 2061, - 1779, - 2232, - 1848, - 2346, - 1864, - 2164, - 1963, - 2078, - 2012, - 2009, - 1873, - 1957, - 2103, - 2010, - 2061, - 2382, - 2078, - 2177, - 1933, - 2284, - 1871, - 2026, - 2009, - 2119, - 1999, - 1943, - 2132, - 1954, - 2068, - 2007, - 2279, - 2037, - 1985, - 2158, - 2072, - 1840, - 2043, - 2071, - 2326, - 2059, - 2498, - 1824, - 2199, - 1898, - 2351, - 1852, - 2237, - 1859, - 2229, - 2003, - 2237, - 1948, - 2101, - 1882, - 2142, - 1886, - 2209, - 1942, - 2162, - 1806, - 2029, - 1852, - 2022, - 1782, - 1968, - 1773, - 1792, - 1781, - 1811, - 1705, - 1831, - 1823, - 1905, - 1712, - 1837, - 1859, - 2050, - 1789, - 1941, - 1803, - 1811, - 1793, - 1884, - 1850, - 1739, - 1823, - 1818, - 1797, - 2081, - 1820, - 1839, - 2015, - 2030, - 1864, - 1760, - 1964, - 1782, - 1962, - 1811, - 1841, - 1685, - 1890, - 1855, - 2021, - 1735, - 2102, - 1814, - 2139, - 1706, - 2252, - 1805, - 2340, - 1832, - 2243, - 1979, - 2341, - 1928, - 1842, - 2243, - 1981, - 2117, - 1927, - 2093, - 1725, - 2056, - 1863, - 1966, - 1918, - 2001, - 2023 - ], - "p50Micros": 1909, - "p95Micros": 2279, - "minMicros": 1652, - "maxMicros": 2580, - "wallMicros": 390388, - "operationsPerSecond": 512.3108292263082 - }, - "orm": { - "samplesMicros": [ - 2988, - 3055, - 2393, - 2114, - 2566, - 2044, - 5168, - 4399, - 2263, - 2344, - 2234, - 2236, - 2146, - 2388, - 2332, - 2512, - 2282, - 2347, - 2440, - 2437, - 2480, - 2275, - 2498, - 2226, - 2478, - 2189, - 2262, - 2759, - 2453, - 2103, - 2340, - 2155, - 2271, - 2409, - 2296, - 2126, - 2439, - 2231, - 2287, - 2203, - 2298, - 2289, - 2305, - 2591, - 2233, - 2457, - 2120, - 2179, - 2000, - 2215, - 2052, - 2263, - 2114, - 2366, - 2211, - 2462, - 2036, - 2351, - 1951, - 2261, - 2002, - 2364, - 1917, - 2218, - 2067, - 2306, - 1990, - 2327, - 1999, - 2084, - 1945, - 2046, - 1972, - 2007, - 1960, - 2020, - 1944, - 2009, - 1977, - 2031, - 1798, - 1979, - 2031, - 1931, - 2391, - 2317, - 2268, - 2247, - 2048, - 2067, - 1873, - 1956, - 2111, - 2382, - 1879, - 2017, - 1910, - 1962, - 2009, - 2023, - 2046, - 1877, - 2194, - 1943, - 2052, - 1784, - 2256, - 1887, - 2054, - 2267, - 2090, - 1895, - 2088, - 1915, - 1997, - 1890, - 2194, - 1856, - 2162, - 2151, - 2300, - 1849, - 2299, - 1905, - 2228, - 1879, - 2224, - 2168, - 1974, - 1857, - 2325, - 1940, - 2220, - 1862, - 2507, - 1914, - 2497, - 2048, - 2345, - 2016, - 2309, - 2442, - 2274, - 2169, - 2119, - 2408, - 2343, - 2233, - 2455, - 2154, - 2345, - 2145, - 2372, - 2084, - 2086, - 2377, - 2050, - 2330, - 2180, - 2204, - 2037, - 2449, - 2102, - 2300, - 2100, - 2379, - 2255, - 2439, - 2161, - 2127, - 2056, - 2400, - 1921, - 2305, - 1881, - 2275, - 1905, - 2207, - 1922, - 1954, - 1960, - 1840, - 1917, - 1884, - 2078, - 1936, - 1863, - 1905, - 1945, - 1967, - 1923, - 1925, - 1825, - 1845, - 2259, - 1861, - 2418, - 1952, - 2505, - 1905 - ], - "p50Micros": 2154, - "p95Micros": 2498, - "minMicros": 1784, - "maxMicros": 5168, - "wallMicros": 437228, - "operationsPerSecond": 457.42724619649243 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 5170, - 5260, - 5342, - 5410, - 10566, - 10698, - 10778, - 10841, - 10505, - 10687, - 10636, - 10963, - 10854, - 10690, - 10924, - 10912, - 10598, - 10538, - 10715, - 10691, - 12190, - 12409, - 12209, - 12387, - 16372, - 16457, - 16400, - 16665, - 14040, - 14104, - 14360, - 14176, - 11502, - 11380, - 11177, - 11388, - 11452, - 11430, - 11470, - 11755, - 10522, - 10757, - 10565, - 10742, - 10510, - 10399, - 10702, - 10618, - 10468, - 10413, - 10379, - 10353, - 10460, - 10979, - 10532, - 10948, - 10450, - 10717, - 10732, - 10819, - 11184, - 11213, - 11247, - 11293, - 11333, - 10946, - 11318, - 11249, - 10927, - 11227, - 11020, - 10966, - 10812, - 10847, - 10866, - 10881, - 10773, - 10983, - 11222, - 11023, - 11126, - 11104, - 11371, - 11337, - 10916, - 11124, - 10879, - 11051, - 10969, - 10967, - 10943, - 10947, - 10820, - 10997, - 10904, - 10896, - 10593, - 10920, - 10677, - 10602, - 10409, - 10996, - 10908, - 10906, - 10776, - 11134, - 11116, - 11181, - 10923, - 10564, - 10534, - 10699, - 11220, - 11117, - 10845, - 11092, - 10800, - 11131, - 10975, - 11246, - 11098, - 11078, - 11081, - 11069, - 10897, - 11204, - 10741, - 11004, - 10549, - 10602, - 10639, - 10674, - 10463, - 10716, - 10966, - 10793, - 10966, - 10896, - 10847, - 10794, - 10647, - 10495, - 10486, - 10849, - 10947, - 11222, - 11154, - 11173, - 11163, - 11216, - 11613, - 11408, - 10469, - 10778, - 10617, - 10640, - 11011, - 11334, - 11598, - 11887, - 11743, - 11753, - 11744, - 11733, - 11057, - 11393, - 11227, - 11207, - 10567, - 10559, - 10919, - 10681, - 10999, - 10946, - 10903, - 10863, - 10920, - 10985, - 11412, - 11237, - 10819, - 10858, - 11143, - 10967, - 11185, - 11545, - 11130, - 11371, - 11116, - 11085, - 10984, - 11151, - 10495, - 10826, - 10552, - 10771, - 10368, - 10512, - 10409, - 10385 - ], - "p50Micros": 10943, - "p95Micros": 12209, - "minMicros": 5170, - "maxMicros": 16665, - "wallMicros": 278425, - "operationsPerSecond": 718.3262997216485 - }, - "orm": { - "samplesMicros": [ - 5783, - 6133, - 6334, - 6556, - 11591, - 11776, - 11916, - 12320, - 11385, - 11332, - 11999, - 11347, - 11827, - 11873, - 11463, - 11979, - 11547, - 11881, - 11704, - 11686, - 11028, - 11414, - 11164, - 11168, - 11185, - 11196, - 11189, - 11192, - 10836, - 11116, - 11399, - 11111, - 11313, - 11655, - 11492, - 11488, - 11722, - 11834, - 11874, - 11982, - 15511, - 15935, - 15788, - 15817, - 15822, - 16246, - 15936, - 15838, - 11579, - 11990, - 11720, - 11703, - 11201, - 11059, - 11315, - 11165, - 11595, - 11624, - 11622, - 11633, - 11599, - 12108, - 12113, - 12027, - 11558, - 11850, - 11736, - 12288, - 12126, - 11943, - 12229, - 12071, - 11763, - 12230, - 12183, - 12054, - 11373, - 11332, - 11335, - 11324, - 11338, - 11336, - 11338, - 11324, - 11585, - 11665, - 11709, - 11749, - 11546, - 11531, - 11530, - 11531, - 11751, - 11655, - 11584, - 11545, - 11399, - 11684, - 11415, - 11652, - 11070, - 11400, - 11286, - 11300, - 10968, - 11237, - 11089, - 11079, - 10854, - 10865, - 10899, - 10921, - 10819, - 11102, - 10991, - 10985, - 11298, - 11631, - 11418, - 12065, - 11791, - 11803, - 11792, - 11790, - 11187, - 11526, - 11392, - 11402, - 10693, - 10985, - 11001, - 11231, - 11138, - 11151, - 11143, - 11126, - 11196, - 11516, - 11284, - 11314, - 11301, - 11787, - 11465, - 11815, - 11313, - 11939, - 11470, - 11437, - 11180, - 11497, - 11814, - 11604, - 11360, - 11742, - 11573, - 11579, - 11503, - 11821, - 12138, - 11734, - 11620, - 12001, - 11782, - 11766, - 11275, - 11584, - 11414, - 11395, - 10901, - 11232, - 11038, - 11033, - 11101, - 11196, - 11263, - 11307, - 11180, - 11488, - 11364, - 11379, - 11467, - 11363, - 11289, - 11251, - 11197, - 11553, - 11265, - 11506, - 11350, - 11364, - 11394, - 11397, - 11480, - 11526, - 11432, - 11712, - 11578, - 11509, - 11356, - 11573 - ], - "p50Micros": 11470, - "p95Micros": 12230, - "minMicros": 5783, - "maxMicros": 16246, - "wallMicros": 291966, - "operationsPerSecond": 685.0112684353658 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 66, - 64, - 64, - 65, - 5150, - 5371, - 5534, - 5692, - 5994, - 5841, - 5741, - 5669, - 5040, - 5077, - 5140, - 5213, - 5990, - 5926, - 5857, - 5816, - 6108, - 6186, - 6300, - 6375, - 5693, - 5555, - 5449, - 5364, - 5025, - 5065, - 5079, - 5092, - 5203, - 5124, - 5119, - 5099, - 5178, - 5143, - 5111, - 5101, - 4902, - 4884, - 4891, - 4899, - 5301, - 5260, - 5249, - 5233, - 5107, - 5203, - 5274, - 5358, - 5604, - 5489, - 5431, - 5381, - 5469, - 5495, - 5506, - 5518, - 5048, - 5406, - 5384, - 5351 - ], - "p50Micros": 5274, - "p95Micros": 6108, - "minMicros": 64, - "maxMicros": 6375, - "wallMicros": 324322 - }, - "sqlMicros": [ - 1226, - 1296, - 1335, - 1361, - 3616, - 3810, - 3974, - 4188, - 1923, - 1752, - 1618, - 1488, - 3870, - 4028, - 4051, - 4096, - 1426, - 1574, - 1340, - 1467, - 3311, - 3457, - 3580, - 3679, - 1634, - 1835, - 1579, - 1450, - 4060, - 4148, - 4200, - 4254, - 1753, - 1716, - 1674, - 1602, - 4069, - 4349, - 4475, - 4609, - 1837, - 1733, - 1587, - 1454, - 3620, - 3785, - 3767, - 3813, - 1503, - 1449, - 1377, - 1314, - 3328, - 3554, - 3602, - 3689, - 1538, - 1705, - 1508, - 1430, - 3360, - 3480, - 3504, - 3616, - 1548, - 1472, - 1404, - 1353, - 3520, - 3606, - 3648, - 3693, - 1354, - 1309, - 1476, - 1287, - 3347, - 3429, - 3490, - 3542, - 1530, - 1657, - 1526, - 1571, - 3569, - 3675, - 3581, - 3608, - 1422, - 1418, - 1401, - 1383, - 3476, - 3603, - 3799, - 3842, - 1764, - 1643, - 1535, - 1423, - 3685, - 3765, - 3892, - 3883, - 1600, - 1573, - 1762, - 1570, - 3595, - 3700, - 3888, - 3849, - 1590, - 1506, - 1347, - 1475, - 3236, - 3477, - 3866, - 3949, - 2137, - 1690, - 1646, - 1615, - 3733, - 3896, - 3939, - 4035 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 85, - 127, - 134, - 140, - 5330, - 5477, - 5655, - 5766, - 5599, - 5636, - 5573, - 5584, - 5739, - 5700, - 5719, - 5727, - 5805, - 5733, - 5700, - 5677, - 5483, - 5559, - 5621, - 5684, - 6001, - 5981, - 5959, - 5948, - 5707, - 5602, - 5569, - 5533, - 5950, - 5934, - 5907, - 5920, - 5223, - 5227, - 5257, - 5270, - 5890, - 5843, - 5831, - 5832, - 5849, - 5850, - 5846, - 5833, - 5544, - 5529, - 5519, - 5542, - 5288, - 5271, - 5269, - 5251, - 5642, - 5627, - 5649, - 5687, - 6056, - 6144, - 6230, - 6229 - ], - "p50Micros": 5655, - "p95Micros": 6056, - "minMicros": 85, - "maxMicros": 6230, - "wallMicros": 341492 - }, - "sqlMicros": [ - 1317, - 1452, - 1582, - 1709, - 3504, - 3601, - 3973, - 3839, - 1785, - 1567, - 1808, - 1569, - 3471, - 3773, - 3796, - 3862, - 1620, - 1893, - 1563, - 1753, - 3610, - 3765, - 3860, - 3944, - 1385, - 1941, - 1850, - 1799, - 3706, - 3705, - 3908, - 3856, - 1797, - 1784, - 1736, - 1691, - 3396, - 3561, - 3682, - 3800, - 1694, - 1978, - 1659, - 1549, - 3835, - 4000, - 4116, - 4210, - 1529, - 1993, - 1862, - 1615, - 3457, - 3712, - 3695, - 3754, - 1556, - 1786, - 1592, - 1541, - 3957, - 4067, - 4312, - 4218, - 1585, - 1552, - 1506, - 1436, - 3265, - 3559, - 3645, - 3670, - 1589, - 1412, - 1618, - 1972, - 3863, - 4084, - 4064, - 4097, - 1951, - 1968, - 1937, - 1874, - 3522, - 3806, - 3759, - 3788, - 1445, - 1661, - 1478, - 1425, - 3730, - 3850, - 3901, - 3952, - 1353, - 1667, - 1607, - 1432, - 3456, - 3562, - 3627, - 3667, - 1652, - 1619, - 1580, - 1532, - 3735, - 3840, - 3857, - 4043, - 1812, - 1787, - 1728, - 1653, - 3944, - 4127, - 4300, - 4396, - 1501, - 1488, - 2138, - 1955, - 3755, - 3772, - 3783, - 3804 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 48 - ], - "sqlMicros": [ - 725, - 1579 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 42 - ], - "sqlMicros": [ - 788, - 1413 - ], - "ormDecodeMicros": [ - 55, - 8 - ], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367479417669, - "endMicros": 1367479426227, - "retainedRootRows": 100, - "rssBefore": 370655232, - "rssAfter": 374652928, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17196128, - "heapCapacity": 27639808, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296041", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 406, - "bytesCurrent": 14592 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 47020, - "bytesCurrent": 3635696 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17297552, - "heapCapacity": 28164096, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296355", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 806, - "bytesCurrent": 28992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47022, - "bytesCurrent": 3637024 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 11766, - "counts": { - "_List": 6165, - "_GrowableList": 4083, - "_Record": 1515, - "_Map": 3 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 4623 - }, - { - "function": "", - "samples": 2406 - }, - { - "function": "wait", - "samples": 1206 - }, - { - "function": "DataRowMessage.parse", - "samples": 1200 - }, - { - "function": "handleMessage", - "samples": 1200 - }, - { - "function": "current", - "samples": 378 - }, - { - "function": "bindParameters", - "samples": 309 - }, - { - "function": "_List._ofGrowableList", - "samples": 303 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 47 - }, - { - "function": "_GrowableList._withData", - "samples": 21 - }, - { - "function": "_List", - "samples": 18 - }, - { - "function": "_init", - "samples": 18 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367479760648, - "endMicros": 1367479771631, - "retainedRootRows": 100, - "rssBefore": 395100160, - "rssAfter": 400801792, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17200480, - "heapCapacity": 28688384, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296391", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 406, - "bytesCurrent": 14592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47021, - "bytesCurrent": 3635984 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17299712, - "heapCapacity": 23969792, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296732", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 806, - "bytesCurrent": 28992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47025, - "bytesCurrent": 3637248 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 16149, - "counts": { - "_List": 8768, - "_GrowableList": 5773, - "_SelectionPlan": 12, - "_Map": 60, - "_Record": 1536 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 6158 - }, - { - "function": "", - "samples": 3306 - }, - { - "function": "wait", - "samples": 1206 - }, - { - "function": "DataRowMessage.parse", - "samples": 1200 - }, - { - "function": "handleMessage", - "samples": 1200 - }, - { - "function": "parameter", - "samples": 636 - }, - { - "function": "_List", - "samples": 375 - }, - { - "function": "_List._ofGrowableList", - "samples": 309 - }, - { - "function": "bindParameters", - "samples": 309 - }, - { - "function": "load", - "samples": 303 - }, - { - "function": "[]", - "samples": 300 - }, - { - "function": "quote", - "samples": 180 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 591, - 576, - 593, - 547, - 564, - 616, - 543, - 456, - 380, - 359, - 362, - 444, - 388, - 465, - 529, - 372, - 341, - 345, - 327, - 334, - 350, - 339, - 377, - 334, - 458, - 385, - 354, - 338, - 366, - 343, - 333, - 328, - 331, - 350, - 354, - 447, - 406, - 347, - 339, - 361, - 346, - 337, - 329, - 360, - 348, - 346, - 340, - 365, - 440, - 360, - 351, - 350, - 332, - 356, - 433, - 388, - 341, - 462, - 452, - 356, - 369, - 348, - 345, - 336, - 346, - 362, - 356, - 337, - 504, - 453, - 378, - 354, - 360, - 342, - 338, - 339, - 346, - 379, - 368, - 469, - 519, - 366, - 344, - 359, - 333, - 349, - 423, - 346, - 343, - 413, - 455, - 328, - 354, - 371, - 363, - 369, - 356, - 361, - 371, - 358, - 374, - 358, - 366, - 349, - 343, - 332, - 340, - 345, - 337, - 378, - 360, - 482, - 424, - 413, - 342, - 355, - 354, - 345, - 351, - 334, - 353, - 367, - 354, - 470, - 435, - 361, - 351, - 352, - 329, - 340, - 331, - 351, - 350, - 341, - 413, - 369, - 357, - 349, - 353, - 335, - 391, - 342, - 337, - 366, - 353, - 391, - 380, - 345, - 341, - 351, - 340, - 338, - 333, - 344, - 343, - 357, - 331, - 422, - 377, - 365, - 363, - 340, - 356, - 332, - 327, - 350, - 346, - 410, - 470, - 361, - 363, - 351, - 338, - 336, - 342, - 342, - 353, - 341, - 342, - 453, - 397, - 347, - 346, - 358, - 339, - 334, - 338, - 328, - 346, - 334, - 326, - 389, - 370, - 350, - 337, - 340, - 334, - 325, - 326, - 350 - ], - "p50Micros": 353, - "p95Micros": 482, - "minMicros": 325, - "maxMicros": 616, - "wallMicros": 74494, - "operationsPerSecond": 2684.7799822804523 - }, - "orm": { - "samplesMicros": [ - 570, - 615, - 572, - 555, - 699, - 667, - 595, - 550, - 375, - 370, - 411, - 412, - 376, - 510, - 444, - 392, - 369, - 380, - 365, - 376, - 373, - 347, - 390, - 365, - 503, - 388, - 416, - 379, - 375, - 359, - 360, - 358, - 351, - 360, - 371, - 364, - 430, - 386, - 356, - 378, - 359, - 358, - 364, - 360, - 368, - 373, - 451, - 465, - 418, - 395, - 366, - 373, - 365, - 377, - 379, - 446, - 356, - 450, - 471, - 413, - 392, - 398, - 372, - 375, - 374, - 388, - 389, - 350, - 464, - 384, - 393, - 348, - 367, - 375, - 353, - 351, - 369, - 381, - 387, - 458, - 612, - 454, - 398, - 396, - 367, - 388, - 373, - 371, - 387, - 382, - 410, - 386, - 383, - 354, - 442, - 380, - 380, - 359, - 382, - 380, - 401, - 400, - 443, - 410, - 376, - 364, - 346, - 359, - 355, - 363, - 354, - 379, - 444, - 381, - 365, - 354, - 353, - 357, - 364, - 351, - 381, - 378, - 458, - 476, - 434, - 378, - 378, - 378, - 350, - 354, - 353, - 354, - 372, - 364, - 388, - 445, - 374, - 360, - 352, - 342, - 381, - 355, - 378, - 376, - 384, - 456, - 386, - 372, - 374, - 368, - 350, - 356, - 348, - 347, - 351, - 369, - 436, - 521, - 408, - 410, - 384, - 356, - 363, - 374, - 361, - 378, - 364, - 481, - 488, - 397, - 352, - 331, - 357, - 335, - 352, - 348, - 371, - 374, - 368, - 450, - 398, - 379, - 357, - 375, - 372, - 348, - 348, - 347, - 360, - 366, - 431, - 429, - 381, - 418, - 388, - 358, - 362, - 336, - 351, - 352 - ], - "p50Micros": 375, - "p95Micros": 510, - "minMicros": 331, - "maxMicros": 699, - "wallMicros": 78725, - "operationsPerSecond": 2540.489044140997 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 583, - 690, - 756, - 784, - 1203, - 1246, - 1281, - 1382, - 1311, - 1234, - 1210, - 1221, - 1378, - 1371, - 1357, - 1276, - 1330, - 1324, - 1263, - 1329, - 1274, - 1293, - 1286, - 1283, - 1380, - 1384, - 1381, - 1379, - 1377, - 1371, - 1373, - 1374, - 1320, - 1308, - 1366, - 1356, - 1321, - 1306, - 1307, - 1313, - 1249, - 1204, - 1262, - 1217, - 1210, - 1215, - 1195, - 1248, - 1301, - 1273, - 1321, - 1295, - 1286, - 1358, - 1316, - 1314, - 1347, - 1377, - 1388, - 1407, - 1511, - 1502, - 1501, - 1500, - 1536, - 1665, - 1599, - 1589, - 1502, - 1482, - 1526, - 1504, - 1313, - 1289, - 1293, - 1295, - 1148, - 1227, - 1255, - 1333, - 1175, - 1258, - 1264, - 1272, - 1202, - 1168, - 1150, - 1223, - 1087, - 1065, - 1058, - 1144, - 1040, - 1102, - 1147, - 1065, - 1034, - 1166, - 1232, - 1158, - 1237, - 1115, - 1269, - 1288, - 1158, - 1282, - 1174, - 1267, - 1243, - 1302, - 1296, - 1512, - 1605, - 1521, - 1665, - 1666, - 1624, - 1554, - 1470, - 1298, - 1265, - 1268, - 1167, - 1237, - 1292, - 1335, - 1351, - 1371, - 1433, - 1450, - 1349, - 1390, - 1320, - 1396, - 1264, - 1360, - 1346, - 1319, - 1418, - 1365, - 1384, - 1262, - 1444, - 1492, - 1310, - 1309, - 1357, - 1279, - 1076, - 1251, - 1117, - 1014, - 1010, - 1240, - 1099, - 1259, - 1229, - 1318, - 1343, - 1324, - 1248, - 1207, - 1324, - 1104, - 1332, - 1389, - 1516, - 1415, - 1563, - 1569, - 1435, - 1421, - 1336, - 1202, - 1030, - 1035, - 1008, - 1052, - 1059, - 1129, - 1032, - 1064, - 1056, - 1042, - 1015, - 1017, - 1009, - 1000, - 998, - 1035, - 1018, - 1068, - 1034, - 1046, - 1129, - 1073, - 1056, - 1058, - 1063, - 1009 - ], - "p50Micros": 1283, - "p95Micros": 1536, - "minMicros": 583, - "maxMicros": 1666, - "wallMicros": 32033, - "operationsPerSecond": 6243.561327381139 - }, - "orm": { - "samplesMicros": [ - 675, - 713, - 760, - 793, - 1319, - 1400, - 1456, - 1492, - 1283, - 1398, - 1357, - 1485, - 1372, - 1352, - 1358, - 1408, - 1483, - 1413, - 1422, - 1384, - 1678, - 1608, - 1705, - 1596, - 1695, - 1661, - 1646, - 1635, - 1490, - 1488, - 1446, - 1524, - 1506, - 1504, - 1500, - 1490, - 1431, - 1430, - 1388, - 1466, - 1325, - 1330, - 1325, - 1321, - 1317, - 1357, - 1370, - 1402, - 1263, - 1416, - 1367, - 1386, - 1244, - 1242, - 1228, - 1294, - 1262, - 1273, - 1283, - 1378, - 1459, - 1394, - 1482, - 1543, - 1539, - 1490, - 1471, - 1589, - 1556, - 1551, - 1537, - 1547, - 1646, - 1669, - 1668, - 1538, - 1540, - 1532, - 1538, - 1368, - 1282, - 1264, - 1268, - 1170, - 1135, - 1269, - 1273, - 1253, - 1208, - 1304, - 1302, - 1300, - 1246, - 1231, - 1241, - 1235, - 1221, - 1162, - 1164, - 1264, - 1228, - 1176, - 1161, - 1249, - 1212, - 1294, - 1322, - 1265, - 1403, - 1369, - 1388, - 1301, - 1396, - 1292, - 1268, - 1221, - 1259, - 1246, - 1389, - 1374, - 1327, - 1409, - 1429, - 1468, - 1350, - 1272, - 1278, - 1312, - 1197, - 1167, - 1172, - 1149, - 1204, - 1153, - 1149, - 1199, - 1307, - 1153, - 1266, - 1189, - 1161, - 1195, - 1208, - 1115, - 1174, - 1268, - 1282, - 1290, - 1272, - 1441, - 1449, - 1455, - 1305, - 1457, - 1473, - 1466, - 1438, - 1463, - 1445, - 1438, - 1360, - 1408, - 1641, - 1575, - 1320, - 1524, - 1602, - 1769, - 1574, - 1532, - 1417, - 1463, - 1541, - 1344, - 1479, - 1328, - 1427, - 1400, - 1615, - 1608, - 1566, - 1526, - 1771, - 1769, - 1653, - 1543, - 1535, - 1613, - 1520, - 1417, - 1214, - 1417, - 1467, - 1378, - 1370, - 1452, - 1488, - 1380, - 1382, - 1220 - ], - "p50Micros": 1380, - "p95Micros": 1646, - "minMicros": 675, - "maxMicros": 1771, - "wallMicros": 35008, - "operationsPerSecond": 5712.979890310786 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 59, - 59, - 62, - 63, - 675, - 739, - 770, - 787, - 851, - 833, - 827, - 846, - 813, - 769, - 761, - 759, - 677, - 674, - 668, - 668, - 686, - 682, - 683, - 691, - 658, - 655, - 652, - 642, - 568, - 572, - 572, - 574, - 440, - 515, - 598, - 616, - 673, - 586, - 551, - 695, - 715, - 696, - 656, - 595, - 634, - 647, - 652, - 555, - 776, - 774, - 760, - 762, - 760, - 787, - 798, - 814, - 685, - 739, - 758, - 756, - 705, - 674, - 707, - 703 - ], - "p50Micros": 682, - "p95Micros": 827, - "minMicros": 59, - "maxMicros": 851, - "wallMicros": 41777 - }, - "sqlMicros": [ - 613, - 670, - 710, - 747, - 723, - 745, - 911, - 841, - 763, - 746, - 742, - 726, - 618, - 685, - 652, - 647, - 627, - 693, - 666, - 666, - 632, - 637, - 634, - 623, - 544, - 530, - 580, - 555, - 410, - 496, - 547, - 625, - 631, - 567, - 524, - 674, - 684, - 680, - 635, - 568, - 612, - 615, - 630, - 529, - 750, - 742, - 739, - 735, - 729, - 734, - 802, - 791, - 597, - 606, - 831, - 774, - 683, - 643, - 688, - 686, - 602, - 546, - 531, - 504 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 53, - 89, - 95, - 101, - 683, - 724, - 762, - 794, - 678, - 678, - 669, - 671, - 699, - 701, - 704, - 707, - 655, - 638, - 637, - 643, - 589, - 615, - 616, - 613, - 576, - 668, - 677, - 693, - 696, - 778, - 795, - 781, - 759, - 671, - 636, - 648, - 635, - 677, - 722, - 719, - 858, - 724, - 676, - 693, - 697, - 655, - 649, - 630, - 707, - 714, - 710, - 697, - 674, - 666, - 665, - 664, - 677, - 673, - 672, - 666, - 652, - 676, - 682, - 698 - ], - "p50Micros": 676, - "p95Micros": 781, - "minMicros": 53, - "maxMicros": 858, - "wallMicros": 41420 - }, - "sqlMicros": [ - 590, - 629, - 662, - 691, - 635, - 644, - 642, - 641, - 652, - 671, - 677, - 675, - 610, - 570, - 649, - 612, - 552, - 544, - 629, - 585, - 537, - 633, - 636, - 665, - 645, - 739, - 764, - 754, - 702, - 629, - 609, - 619, - 598, - 631, - 643, - 731, - 794, - 672, - 642, - 660, - 585, - 673, - 611, - 599, - 666, - 683, - 678, - 670, - 587, - 675, - 601, - 673, - 633, - 646, - 640, - 643, - 604, - 621, - 694, - 672, - 628, - 587, - 564, - 536 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 25 - ], - "sqlMicros": [ - 328 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 24 - ], - "sqlMicros": [ - 353 - ], - "ormDecodeMicros": [ - 1 - ], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1367480167582, - "endMicros": 1367480169351, - "retainedRootRows": 10, - "rssBefore": 364445696, - "rssAfter": 364642304, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17112512, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296794", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 16, - "bytesCurrent": 512 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 47038, - "bytesCurrent": 3636560 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17111696, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789456296987", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 26, - "bytesCurrent": 832 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47033, - "bytesCurrent": 3636096 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 487, - "counts": { - "_List": 274, - "_GrowableList": 177, - "_Record": 36 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 234 - }, - { - "function": "", - "samples": 63 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "wait", - "samples": 33 - }, - { - "function": "DataRowMessage.parse", - "samples": 30 - }, - { - "function": "handleMessage", - "samples": 30 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "_List", - "samples": 9 - }, - { - "function": "_GrowableList._withData", - "samples": 9 - }, - { - "function": "_GrowableList._ofEfficientLengthIterable", - "samples": 6 - }, - { - "function": "_List.filled", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1367480394044, - "endMicros": 1367480396502, - "retainedRootRows": 10, - "rssBefore": 373932032, - "rssAfter": 374210560, - "processLifetimeMaxRss": 484982784 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17111200, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789456297020", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 16, - "bytesCurrent": 512 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47032, - "bytesCurrent": 3635952 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17122720, - "heapCapacity": 23445504, - "externalUsage": 272 - }, - "gcTimestamp": "1789456297224", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 26, - "bytesCurrent": 832 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47036, - "bytesCurrent": 3637904 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 806, - "counts": { - "_List": 437, - "_GrowableList": 303, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 45 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 366 - }, - { - "function": "", - "samples": 63 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "quote", - "samples": 42 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "wait", - "samples": 33 - }, - { - "function": "_write", - "samples": 30 - }, - { - "function": "DataRowMessage.parse", - "samples": 30 - }, - { - "function": "handleMessage", - "samples": 30 - }, - { - "function": "_List", - "samples": 25 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - } - ] - }, - { - "scenario": "postgres_delay_20ms", - "environment": { - "backend": "postgres_delay_20ms", - "databaseVersion": "18.4 (Homebrew)", - "pid": 5186 - }, - "oneWayDelayMs": 10, - "echoRoundTripMicros": { - "samplesMicros": [ - 24442, - 24545, - 23766, - 24729, - 24725, - 24852, - 24477, - 24819, - 25129, - 24860 - ], - "p50Micros": 24725, - "p95Micros": 25129, - "minMicros": 23766, - "maxMicros": 25129, - "wallMicros": 246344 - }, - "selectOneMicros": { - "samplesMicros": [ - 77574, - 75453, - 75477, - 74005, - 74697, - 74833, - 74584, - 75129, - 75531, - 75403 - ], - "p50Micros": 75129, - "p95Micros": 77574, - "minMicros": 74005, - "maxMicros": 77574, - "wallMicros": 752686, - "operationsPerSecond": 13.28575262460043 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 77194, - 80261, - 77152, - 76208, - 73663, - 75031, - 77315, - 76417, - 74503, - 76444, - 75343, - 74414, - 74517, - 77633, - 77800, - 77544, - 77922, - 75523, - 76456, - 75724, - 76745, - 76337, - 76649, - 76771, - 74783, - 76587, - 75441, - 76730, - 74830, - 75338, - 76552, - 76157, - 77255, - 75888, - 74830, - 75284, - 77326, - 76119, - 75491, - 77412, - 77190, - 74671, - 74917, - 76574, - 76340, - 77780, - 77274, - 76129, - 74664, - 75034, - 77144, - 77002, - 77502, - 75804, - 76842, - 75243, - 74154, - 75968, - 75849, - 76648, - 76233, - 74795, - 76335, - 75114, - 75494, - 78799, - 77169, - 75524, - 77878, - 75277, - 76430, - 75314, - 76447, - 76253, - 76521, - 76695, - 76438, - 74912, - 74684, - 76884, - 76500, - 76270, - 76902, - 76027, - 77010, - 75024, - 75522, - 74151, - 74880, - 75479, - 76533, - 76713, - 76735, - 74307, - 75332, - 76523, - 76413, - 76168, - 77124, - 76000, - 75845, - 76301, - 76747, - 76888, - 78126, - 76432, - 77602, - 73972, - 74473, - 75286, - 75264, - 76294, - 76604, - 77883, - 77848, - 74982, - 74345, - 76724, - 75445, - 75504, - 75397, - 75298, - 75557, - 74107, - 76636, - 75427, - 77367, - 74754, - 75575, - 76273, - 75739, - 76548, - 77041, - 76293, - 76325, - 75806, - 77868, - 75190, - 74029, - 75663, - 76543, - 76680, - 75900, - 78068, - 75476, - 75027, - 74264, - 75316, - 77635, - 80965, - 76255, - 75508, - 75887, - 75099, - 76321, - 78108, - 75990, - 75555, - 74998, - 74572, - 75068, - 76068, - 75213, - 76818, - 76865, - 75373, - 77333, - 72790, - 74155, - 75545, - 76360, - 77764, - 77689, - 77068, - 76366, - 74396, - 74883, - 75177, - 75504, - 76021, - 75814, - 76009, - 74539, - 72945, - 74208, - 77141, - 76343, - 76846, - 74725, - 76678, - 75037, - 75685, - 74753, - 74624, - 75411, - 75335, - 75555, - 74307, - 73671, - 77138 - ], - "p50Micros": 76021, - "p95Micros": 77848, - "minMicros": 72790, - "maxMicros": 80965, - "wallMicros": 15201972, - "operationsPerSecond": 13.15618789457052 - }, - "orm": { - "samplesMicros": [ - 77042, - 79142, - 78950, - 77662, - 75725, - 77273, - 76004, - 75297, - 75449, - 76473, - 77802, - 76339, - 76121, - 76012, - 76880, - 77617, - 77933, - 76420, - 75371, - 74574, - 76891, - 76158, - 76117, - 78359, - 77108, - 77115, - 75186, - 74906, - 77804, - 78788, - 77050, - 80318, - 76520, - 76837, - 75011, - 76555, - 77168, - 78033, - 77294, - 77639, - 75939, - 75682, - 76475, - 78530, - 75995, - 78444, - 76981, - 76335, - 75150, - 74643, - 76220, - 77222, - 77432, - 78311, - 77615, - 77410, - 74130, - 75976, - 78021, - 77211, - 76327, - 76949, - 76498, - 75491, - 76896, - 76847, - 77726, - 77267, - 77729, - 76527, - 76755, - 75495, - 76798, - 76032, - 75743, - 78146, - 76727, - 74266, - 74718, - 76784, - 78670, - 76409, - 77264, - 76517, - 76289, - 75652, - 75269, - 74741, - 74918, - 74330, - 76778, - 77122, - 75685, - 75324, - 75366, - 77532, - 76433, - 78126, - 78941, - 76944, - 75400, - 76001, - 76194, - 77068, - 77887, - 77216, - 76298, - 76275, - 74206, - 75791, - 75983, - 77742, - 76686, - 78263, - 77249, - 75247, - 76196, - 77133, - 78232, - 78655, - 76765, - 77601, - 75748, - 75187, - 75471, - 75437, - 75960, - 76280, - 77888, - 77090, - 73350, - 74025, - 76442, - 76232, - 76969, - 76242, - 75059, - 76048, - 75800, - 75349, - 76521, - 76980, - 77526, - 75377, - 76234, - 74452, - 75822, - 76643, - 79230, - 76238, - 76280, - 76918, - 74989, - 74729, - 77477, - 75631, - 77093, - 76739, - 75817, - 76383, - 75123, - 74349, - 77401, - 74808, - 77545, - 74595, - 75080, - 74629, - 75289, - 75123, - 77476, - 76898, - 79933, - 77035, - 75109, - 75027, - 76042, - 76505, - 75697, - 76514, - 75745, - 74923, - 74297, - 74875, - 75278, - 74016, - 75957, - 76740, - 76120, - 75430, - 74770, - 75564, - 77193, - 76735, - 75606, - 75438, - 76031, - 75207, - 74768, - 75689 - ], - "p50Micros": 76327, - "p95Micros": 78444, - "minMicros": 73350, - "maxMicros": 80318, - "wallMicros": 15277000, - "operationsPerSecond": 13.091575571119984 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 86313, - 86747, - 86797, - 86930, - 164153, - 164627, - 164788, - 164936, - 157768, - 157442, - 157731, - 158021, - 157929, - 158390, - 159160, - 158898, - 158161, - 158238, - 158241, - 158299, - 159050, - 159076, - 159085, - 159045, - 158672, - 158972, - 159754, - 159292, - 159266, - 159253, - 159122, - 159402, - 157965, - 157913, - 158778, - 157882, - 154725, - 154756, - 155315, - 154809, - 152579, - 153012, - 152660, - 152824, - 151376, - 151440, - 151454, - 151476, - 149817, - 149836, - 149860, - 149908, - 152498, - 153162, - 152954, - 153430, - 156653, - 157368, - 157313, - 157375, - 157920, - 158450, - 158899, - 158230, - 158321, - 159097, - 158997, - 158842, - 157669, - 158762, - 158481, - 158735, - 158722, - 159283, - 159752, - 159472, - 158422, - 158805, - 159454, - 158816, - 157390, - 157743, - 158075, - 157609, - 156441, - 156369, - 156398, - 156783, - 155219, - 155697, - 156061, - 155844, - 155138, - 155033, - 154974, - 154922, - 152468, - 152991, - 153409, - 152968, - 151799, - 152085, - 152476, - 152427, - 153176, - 153639, - 153559, - 153721, - 154364, - 154901, - 154760, - 154714, - 155542, - 155939, - 156191, - 156556, - 155933, - 156001, - 156015, - 156715, - 155757, - 156678, - 156525, - 156330, - 157333, - 157657, - 157759, - 158383, - 157404, - 157529, - 157542, - 158391, - 158484, - 158013, - 158428, - 158811, - 157105, - 157793, - 157444, - 157802, - 156485, - 156198, - 156574, - 156277, - 157225, - 158024, - 157731, - 157749, - 156635, - 156569, - 156514, - 156482, - 152188, - 152044, - 152903, - 152334, - 149936, - 150042, - 150434, - 150126, - 151239, - 151372, - 151629, - 151873, - 153303, - 153737, - 153688, - 154183, - 155414, - 155910, - 156242, - 156037, - 156485, - 156323, - 156564, - 156459, - 155263, - 155714, - 155695, - 156202, - 156679, - 156402, - 156819, - 156843, - 157311, - 157858, - 157583, - 157815, - 158656, - 158698, - 158564, - 158845, - 158103, - 158472, - 158899, - 158429, - 156803, - 157039, - 156823, - 157515 - ], - "p50Micros": 156678, - "p95Micros": 159283, - "minMicros": 86313, - "maxMicros": 164936, - "wallMicros": 3913504, - "operationsPerSecond": 51.10509660907463 - }, - "orm": { - "samplesMicros": [ - 78272, - 78819, - 78985, - 79292, - 156065, - 156492, - 156679, - 156947, - 155291, - 154686, - 155092, - 155415, - 151506, - 151959, - 151657, - 152635, - 149445, - 149546, - 150315, - 150173, - 149326, - 149925, - 150162, - 150672, - 152138, - 152539, - 152609, - 153365, - 154659, - 154951, - 155986, - 155849, - 152390, - 153504, - 153033, - 153047, - 154613, - 155696, - 155114, - 155560, - 160138, - 159974, - 159890, - 159916, - 160205, - 161264, - 160348, - 160778, - 158929, - 159619, - 159337, - 160412, - 157737, - 158349, - 158933, - 158312, - 157215, - 157567, - 158474, - 157810, - 156434, - 157722, - 157790, - 158575, - 156816, - 157740, - 158447, - 157813, - 154705, - 156214, - 154985, - 155579, - 151475, - 151096, - 151081, - 151514, - 150414, - 150899, - 150799, - 151524, - 153069, - 153206, - 153523, - 154098, - 154181, - 154994, - 154791, - 154882, - 154089, - 154679, - 155497, - 155446, - 157331, - 158589, - 160112, - 160181, - 161475, - 162860, - 162388, - 162793, - 161627, - 158637, - 160386, - 159611, - 159058, - 158876, - 159259, - 159123, - 158020, - 158849, - 158519, - 160057, - 157768, - 157363, - 157764, - 158152, - 156918, - 158070, - 157284, - 158116, - 157849, - 157896, - 158295, - 159090, - 156245, - 156370, - 157434, - 157222, - 152762, - 153247, - 152970, - 153946, - 151849, - 152524, - 152569, - 153237, - 153326, - 154016, - 154665, - 154612, - 156492, - 157619, - 157607, - 158522, - 158848, - 160144, - 160744, - 161743, - 156141, - 156004, - 156258, - 156274, - 154939, - 155949, - 157909, - 155776, - 160111, - 159926, - 159537, - 160045, - 164228, - 165311, - 165327, - 166397, - 162031, - 163980, - 164039, - 165275, - 158914, - 158578, - 158578, - 159289, - 160864, - 158788, - 159787, - 161068, - 158876, - 159362, - 160164, - 161327, - 159465, - 160433, - 161101, - 160537, - 156770, - 157606, - 159296, - 158686, - 153581, - 153289, - 153097, - 153329, - 149380, - 150630, - 150515, - 150502, - 151387, - 151244, - 151208, - 151182 - ], - "p50Micros": 156816, - "p95Micros": 162031, - "minMicros": 78272, - "maxMicros": 166397, - "wallMicros": 3914576, - "operationsPerSecond": 51.091101565022626 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 127, - 136, - 138, - 141, - 73492, - 73655, - 73751, - 73941, - 75899, - 75802, - 76011, - 76095, - 77723, - 77657, - 77697, - 77626, - 77888, - 77864, - 77753, - 77675, - 78071, - 78256, - 78266, - 78290, - 78899, - 78487, - 78438, - 78421, - 79718, - 79704, - 79722, - 79782, - 79268, - 79054, - 79116, - 79112, - 77864, - 77958, - 78021, - 77990, - 79196, - 78934, - 78830, - 79104, - 78626, - 78419, - 78368, - 78213, - 76858, - 76846, - 76871, - 76896, - 75602, - 75499, - 75562, - 75626, - 75117, - 75023, - 74962, - 74909, - 77050, - 76878, - 76851, - 76827 - ], - "p50Micros": 77697, - "p95Micros": 79704, - "minMicros": 127, - "maxMicros": 79782, - "wallMicros": 4642575 - }, - "sqlMicros": [ - 73128, - 73491, - 73627, - 73672, - 75054, - 75557, - 76002, - 76235, - 76917, - 76987, - 77917, - 78000, - 77379, - 77133, - 77600, - 78263, - 77454, - 78346, - 77970, - 78307, - 77654, - 78146, - 78968, - 78648, - 79279, - 79253, - 79741, - 79767, - 78657, - 78779, - 79356, - 78978, - 77410, - 77763, - 77568, - 77987, - 78186, - 79177, - 78479, - 79146, - 77944, - 78557, - 78423, - 77886, - 76592, - 76566, - 76780, - 77001, - 75143, - 75129, - 75450, - 75766, - 74777, - 74698, - 74656, - 75153, - 76815, - 76673, - 76644, - 77006, - 76287, - 76802, - 76614, - 76825 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 218, - 525, - 581, - 614, - 77296, - 77568, - 77716, - 77875, - 77003, - 76957, - 76994, - 77020, - 76628, - 76444, - 76355, - 76317, - 76974, - 77033, - 77114, - 77156, - 77869, - 77685, - 77671, - 77660, - 78437, - 78483, - 78499, - 78611, - 77749, - 77720, - 77853, - 77884, - 80398, - 80254, - 80098, - 79938, - 78381, - 78122, - 78142, - 78171, - 78034, - 77686, - 77586, - 77525, - 75794, - 75784, - 75783, - 75762, - 76811, - 76710, - 76724, - 76912, - 77708, - 77836, - 77867, - 77794, - 77981, - 77816, - 77831, - 77992, - 79149, - 79470, - 79498, - 79351 - ], - "p50Micros": 77686, - "p95Micros": 79938, - "minMicros": 218, - "maxMicros": 80398, - "wallMicros": 4665417 - }, - "sqlMicros": [ - 76468, - 77074, - 77138, - 77239, - 76122, - 76789, - 77203, - 76854, - 76231, - 76146, - 76086, - 76568, - 76526, - 76774, - 77076, - 77011, - 77391, - 77553, - 77512, - 77506, - 77458, - 78280, - 78707, - 78619, - 76678, - 77788, - 77766, - 77675, - 79246, - 79427, - 79901, - 80758, - 77107, - 78174, - 77803, - 78321, - 77516, - 77577, - 77296, - 77593, - 75408, - 75650, - 75554, - 75804, - 76155, - 76784, - 76739, - 76769, - 76609, - 77821, - 77847, - 77906, - 77009, - 77246, - 78156, - 78116, - 78731, - 78974, - 78978, - 79536, - 79464, - 78584, - 78845, - 79173 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 152 - ], - "sqlMicros": [ - 78114 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 140 - ], - "sqlMicros": [ - 75166 - ], - "ormDecodeMicros": [ - 89 - ], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 75156, - 75234, - 76824, - 80889, - 82298, - 82085, - 76196, - 81333, - 82328, - 81400, - 78701, - 76769, - 80065, - 79285, - 79787, - 80610, - 80605, - 80435, - 79987, - 78053, - 78683, - 80067, - 80623, - 80774, - 79094, - 81918, - 80469, - 80319, - 79953, - 80821, - 79935, - 82693, - 83650, - 80122, - 81162, - 79389, - 80356, - 82147, - 83039, - 83834, - 83235, - 74470, - 77185, - 81546, - 80126, - 83178, - 78279, - 81715, - 79742, - 80020, - 76747, - 81207, - 81660, - 78920, - 77997, - 79233, - 79879, - 78457, - 81795, - 85055, - 81634, - 80304, - 77592, - 79142, - 80535, - 80605, - 83626, - 78478, - 77223, - 78860, - 76509, - 80532, - 81861, - 82202, - 82480, - 81044, - 82216, - 79705, - 79877, - 78811, - 79902, - 82558, - 80781, - 80051, - 76008, - 79342, - 81535, - 79334, - 83691, - 82678, - 80274, - 80126, - 80766, - 80163, - 79497, - 82296, - 81083, - 81530, - 78593, - 81748, - 81716, - 83324, - 81536, - 80236, - 80844, - 80038, - 78542, - 79132, - 83453, - 82488, - 83366, - 80508, - 77994, - 79742, - 80604, - 83877, - 80554, - 80249, - 81577, - 79817, - 78545, - 81150, - 77874, - 80786, - 78847, - 78552, - 77891, - 81995, - 79005, - 83596, - 79168, - 79496, - 79267, - 77881, - 78235, - 84431, - 82614, - 79497, - 81394, - 81141, - 80066, - 77299, - 80600, - 81152, - 81877, - 79183, - 82293, - 79993, - 79501, - 81403, - 79881, - 78530, - 80530, - 82288, - 77020, - 79723, - 78225, - 81054, - 82556, - 81915, - 83169, - 78698, - 78241, - 78507, - 80503, - 80715, - 80546, - 78270, - 81428, - 78051, - 80962, - 82519, - 81451, - 83314, - 81791, - 83050, - 79535, - 80488, - 80641, - 81808, - 80607, - 82798, - 78415, - 74648, - 79868, - 80941, - 80273, - 82308, - 80379, - 82309, - 79007, - 76230, - 76137, - 79996, - 80053, - 81612, - 81634, - 79898, - 77946, - 80810 - ], - "p50Micros": 80379, - "p95Micros": 83324, - "minMicros": 74470, - "maxMicros": 85055, - "wallMicros": 16058333, - "operationsPerSecond": 12.454592889560828 - }, - "orm": { - "samplesMicros": [ - 75320, - 74721, - 82200, - 82095, - 83502, - 81114, - 76742, - 81719, - 82888, - 79375, - 80708, - 81048, - 80877, - 79643, - 80291, - 80800, - 80355, - 83357, - 80848, - 81198, - 79921, - 80887, - 77685, - 79515, - 78157, - 79443, - 81315, - 81443, - 82297, - 79308, - 81715, - 80972, - 81084, - 84287, - 78138, - 77960, - 81075, - 81798, - 84384, - 83657, - 81833, - 79558, - 81261, - 83464, - 82783, - 79292, - 81784, - 79406, - 78675, - 79299, - 81978, - 81897, - 82181, - 82754, - 78476, - 78987, - 77141, - 79429, - 83147, - 82169, - 82715, - 78779, - 79826, - 78351, - 80902, - 81839, - 80581, - 80614, - 79572, - 79942, - 81974, - 79106, - 81350, - 76461, - 82741, - 83162, - 79846, - 79177, - 80457, - 79746, - 80831, - 80447, - 79494, - 77058, - 79235, - 79301, - 81559, - 82506, - 81792, - 79537, - 77660, - 78312, - 80965, - 80595, - 81050, - 82206, - 82535, - 81238, - 80139, - 78555, - 79965, - 84405, - 83261, - 82175, - 77726, - 77905, - 81010, - 84125, - 77930, - 80297, - 81077, - 83621, - 79688, - 76097, - 82900, - 78577, - 84810, - 85339, - 78717, - 78439, - 81407, - 80108, - 81790, - 79419, - 80440, - 78757, - 78598, - 77695, - 79913, - 78944, - 82473, - 79753, - 80988, - 79787, - 79728, - 79911, - 84633, - 83137, - 78301, - 80692, - 83522, - 81216, - 81955, - 80825, - 86869, - 78143, - 83222, - 80147, - 81112, - 78828, - 84421, - 82442, - 81985, - 78774, - 81213, - 76422, - 82002, - 83015, - 79451, - 81741, - 77386, - 82194, - 79192, - 78228, - 81779, - 82297, - 81006, - 82129, - 77312, - 79739, - 79070, - 80066, - 80513, - 77284, - 80265, - 82333, - 76687, - 76098, - 78780, - 80930, - 83257, - 79693, - 79127, - 80969, - 77223, - 80378, - 81610, - 83712, - 79130, - 80780, - 79645, - 81354, - 82068, - 81673, - 78995, - 82519, - 80291, - 81692, - 73289, - 78564 - ], - "p50Micros": 80614, - "p95Micros": 83657, - "minMicros": 73289, - "maxMicros": 86869, - "wallMicros": 16102612, - "operationsPerSecond": 12.420345221011349 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 76857, - 76964, - 77063, - 77141, - 158883, - 159210, - 159368, - 159481, - 163466, - 163694, - 163752, - 164236, - 161647, - 161468, - 161740, - 161619, - 162647, - 163291, - 163013, - 163019, - 163175, - 162950, - 163443, - 163537, - 164247, - 164313, - 164351, - 165171, - 163742, - 163459, - 163446, - 164102, - 164577, - 165392, - 165279, - 165181, - 161760, - 162149, - 161805, - 162554, - 155119, - 154876, - 155045, - 155231, - 156081, - 156191, - 156620, - 156524, - 158597, - 158938, - 158972, - 159333, - 159791, - 160390, - 160308, - 160300, - 161930, - 162276, - 162727, - 162654, - 166553, - 167193, - 166951, - 167147, - 162932, - 163111, - 163764, - 163665, - 164150, - 164053, - 164391, - 164442, - 167120, - 167960, - 167807, - 167771, - 166761, - 167520, - 167375, - 167216, - 167056, - 167491, - 168038, - 167924, - 165582, - 165569, - 165830, - 166429, - 169104, - 169569, - 170171, - 170010, - 165505, - 165867, - 166242, - 165814, - 158248, - 159030, - 158664, - 158867, - 160076, - 160747, - 160518, - 160752, - 162630, - 162927, - 163185, - 162967, - 161300, - 162094, - 162000, - 161995, - 163298, - 163693, - 163771, - 163894, - 169165, - 168899, - 169177, - 169449, - 168160, - 168228, - 168778, - 169526, - 165428, - 166101, - 165907, - 165709, - 164152, - 164094, - 164820, - 164496, - 163544, - 163579, - 163630, - 163652, - 165910, - 166056, - 166222, - 166782, - 166735, - 167280, - 166985, - 167377, - 163331, - 164477, - 164126, - 163769, - 163539, - 164588, - 164392, - 164264, - 160777, - 160230, - 160726, - 160642, - 158145, - 157850, - 158085, - 158310, - 160444, - 160378, - 160438, - 160712, - 162229, - 162836, - 162816, - 162709, - 163161, - 163574, - 163440, - 164095, - 164229, - 164521, - 164633, - 165546, - 164910, - 165685, - 165388, - 165624, - 166298, - 166248, - 166776, - 166414, - 166902, - 167492, - 167449, - 167366, - 166757, - 166233, - 166759, - 167522, - 163928, - 163995, - 164709, - 164548, - 164049, - 162207, - 163518, - 163809 - ], - "p50Micros": 163769, - "p95Micros": 168228, - "minMicros": 76857, - "maxMicros": 170171, - "wallMicros": 4091151, - "operationsPerSecond": 48.88599809686809 - }, - "orm": { - "samplesMicros": [ - 81842, - 81975, - 82064, - 82149, - 158079, - 158259, - 158331, - 158415, - 154517, - 154828, - 154754, - 155236, - 154780, - 155234, - 155537, - 155391, - 159625, - 160842, - 161190, - 161618, - 162530, - 163188, - 163660, - 163403, - 159654, - 161623, - 159824, - 159762, - 162764, - 162785, - 163135, - 163885, - 165789, - 166876, - 166626, - 166453, - 168859, - 169417, - 169756, - 170297, - 166101, - 166246, - 167234, - 166741, - 165226, - 166431, - 166077, - 166418, - 166311, - 166436, - 166930, - 167553, - 161584, - 162970, - 162688, - 162655, - 162859, - 163467, - 163406, - 163936, - 159943, - 159799, - 160765, - 160139, - 157563, - 157480, - 157410, - 157599, - 159503, - 160070, - 159818, - 160109, - 158145, - 158083, - 158141, - 158502, - 157469, - 157348, - 157614, - 157891, - 160491, - 161041, - 161518, - 161107, - 161143, - 161842, - 162569, - 162414, - 163040, - 163915, - 165593, - 165667, - 168152, - 168409, - 169072, - 169843, - 167938, - 170670, - 169049, - 170284, - 169119, - 170003, - 170741, - 170034, - 167379, - 167303, - 167301, - 167305, - 169186, - 169250, - 170276, - 169269, - 166005, - 166738, - 166147, - 166392, - 159462, - 159815, - 160352, - 159847, - 159740, - 160394, - 160071, - 160072, - 155560, - 155772, - 155842, - 156608, - 156333, - 156520, - 156791, - 157384, - 164740, - 164633, - 164928, - 165231, - 167087, - 167150, - 167174, - 167185, - 164698, - 164206, - 164512, - 164810, - 165565, - 165292, - 165359, - 165972, - 165233, - 166530, - 166292, - 166588, - 165045, - 164935, - 165704, - 165728, - 164299, - 165265, - 166211, - 165288, - 163423, - 163246, - 163650, - 164841, - 163747, - 164394, - 164811, - 166168, - 164830, - 165607, - 165037, - 165434, - 162644, - 163152, - 163746, - 162562, - 159375, - 159957, - 159631, - 159835, - 160553, - 161395, - 161127, - 161361, - 165223, - 166702, - 166708, - 167186, - 170741, - 171786, - 171848, - 172194, - 169890, - 169835, - 171401, - 169825, - 163441, - 161833, - 162927, - 162487 - ], - "p50Micros": 163650, - "p95Micros": 170034, - "minMicros": 81842, - "maxMicros": 172194, - "wallMicros": 4086091, - "operationsPerSecond": 48.946535943521575 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 300, - 225, - 217, - 219, - 81041, - 81206, - 81320, - 81448, - 83210, - 83165, - 83190, - 83210, - 82944, - 82770, - 82707, - 82672, - 81429, - 81445, - 81478, - 81510, - 83035, - 82929, - 82915, - 82918, - 85119, - 85236, - 85241, - 85248, - 79818, - 79484, - 79419, - 79406, - 77318, - 77230, - 77195, - 77154, - 80027, - 80130, - 80191, - 80253, - 80364, - 80285, - 80318, - 80389, - 81603, - 81384, - 81360, - 81321, - 83394, - 83456, - 83488, - 83522, - 82290, - 82111, - 82144, - 82165, - 82429, - 82356, - 82304, - 82233, - 80667, - 80594, - 80570, - 80576 - ], - "p50Micros": 81445, - "p95Micros": 85119, - "minMicros": 217, - "maxMicros": 85248, - "wallMicros": 4897295 - }, - "sqlMicros": [ - 80736, - 80939, - 81052, - 81182, - 82707, - 83223, - 82942, - 83213, - 82218, - 82704, - 82996, - 82725, - 81129, - 81148, - 81382, - 81584, - 82307, - 82703, - 83145, - 83068, - 84493, - 85235, - 85062, - 85396, - 79218, - 79151, - 79835, - 79426, - 76875, - 77305, - 77164, - 77287, - 79776, - 79986, - 80175, - 80175, - 80009, - 79963, - 80181, - 80494, - 81116, - 80984, - 81625, - 81369, - 82687, - 83206, - 83496, - 83770, - 81959, - 81686, - 82171, - 82196, - 81633, - 82059, - 82393, - 82668, - 80186, - 80514, - 80792, - 80495, - 83205, - 82998, - 82930, - 83212 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 181, - 288, - 313, - 333, - 81638, - 81845, - 81939, - 82023, - 80508, - 80443, - 80458, - 80544, - 79915, - 79845, - 79817, - 79740, - 76560, - 76517, - 76573, - 76607, - 77364, - 77268, - 77188, - 77142, - 77574, - 77582, - 77597, - 77604, - 80219, - 80232, - 80281, - 80327, - 80847, - 81090, - 81188, - 81211, - 83143, - 82992, - 82784, - 83038, - 83898, - 83795, - 83918, - 83716, - 82586, - 82525, - 82410, - 82332, - 83100, - 82997, - 82980, - 82984, - 83470, - 83402, - 83586, - 83604, - 80752, - 80736, - 80715, - 80740, - 81738, - 81532, - 81335, - 81212 - ], - "p50Micros": 80752, - "p95Micros": 83716, - "minMicros": 181, - "maxMicros": 83918, - "wallMicros": 4852821 - }, - "sqlMicros": [ - 81201, - 81460, - 81586, - 81771, - 79955, - 80485, - 80478, - 80394, - 79184, - 79635, - 79847, - 80093, - 76138, - 76251, - 76643, - 76612, - 77115, - 77198, - 77004, - 77175, - 77158, - 77584, - 77444, - 77675, - 79664, - 80251, - 80102, - 80407, - 80260, - 81010, - 81069, - 81097, - 81876, - 83152, - 82633, - 83230, - 83482, - 83095, - 83600, - 84171, - 81422, - 82552, - 82235, - 82681, - 82277, - 82852, - 82713, - 83359, - 82847, - 83006, - 83451, - 83986, - 79697, - 80763, - 80315, - 81033, - 80578, - 81144, - 81778, - 81628, - 83113, - 83512, - 83452, - 83367 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 124 - ], - "sqlMicros": [ - 79432 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 139 - ], - "sqlMicros": [ - 79956 - ], - "ormDecodeMicros": [ - 52 - ], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 165355, - 159259, - 160315, - 162987, - 157265, - 161559, - 164362, - 160835, - 161545, - 168928, - 164460, - 160586, - 158818, - 162371, - 165588, - 158386, - 161258, - 164810, - 167170, - 164626, - 168199, - 158637, - 159533, - 163989, - 161947, - 161577, - 162399, - 161686, - 160679, - 158822, - 166138, - 168534, - 160677, - 168513, - 165310, - 165984, - 159498, - 164457, - 164821, - 156529, - 163769, - 160351, - 159467, - 162411, - 162332, - 164515, - 161481, - 163949, - 165078, - 165133, - 161284, - 161503, - 160647, - 160956, - 159497, - 165387, - 158532, - 163540, - 163156, - 165538, - 166277, - 161533, - 159100, - 164334, - 160795, - 168446, - 163755, - 160581, - 159386, - 158793, - 164450, - 160145, - 163393, - 165957, - 160090, - 158871, - 162096, - 162400, - 151152, - 165604, - 160723, - 155992, - 160405, - 161479, - 164544, - 160399, - 165791, - 166643, - 163770, - 165240, - 162550, - 161043, - 162557, - 161332, - 161234, - 158110, - 160998, - 161610, - 155327, - 159468, - 165043, - 162332, - 162103, - 164247, - 166067, - 162015, - 159159, - 159892, - 162375, - 161687, - 158485, - 167496, - 160308, - 162847, - 164031, - 162917, - 163990, - 161247, - 162926, - 166041, - 161902, - 165720, - 162940, - 160493, - 162023, - 159385, - 163250, - 159780, - 162776, - 162404, - 165580, - 162353, - 161852, - 164038, - 158669, - 162120, - 164595, - 156223, - 157619, - 162154, - 166362, - 160748, - 163899, - 163829, - 164811, - 163605, - 161503, - 162314, - 157723, - 160860, - 161420, - 156525, - 159383, - 161289, - 165883, - 160707, - 166227, - 162958, - 160600, - 160439, - 164520, - 162980, - 160854, - 165339, - 163696, - 162835, - 158565, - 157994, - 165240, - 158581, - 158379, - 164153, - 160452, - 161161, - 158281, - 160114, - 158306, - 165215, - 162818, - 163128, - 161674, - 162280, - 164138, - 157913, - 158599, - 164502, - 164024, - 158763, - 162318, - 163924, - 161490, - 162800, - 158918, - 160724, - 155931, - 161997, - 167824, - 159087, - 162879, - 165827 - ], - "p50Micros": 162096, - "p95Micros": 166277, - "minMicros": 151152, - "maxMicros": 168928, - "wallMicros": 32423703, - "operationsPerSecond": 6.1683269181191305 - }, - "orm": { - "samplesMicros": [ - 158963, - 158877, - 163296, - 164490, - 158368, - 162033, - 167987, - 166779, - 158531, - 161134, - 166773, - 165586, - 158784, - 164172, - 160992, - 160587, - 164863, - 166335, - 161980, - 160194, - 164606, - 164690, - 162345, - 163421, - 165591, - 164636, - 161312, - 161564, - 162583, - 161033, - 162359, - 168931, - 161073, - 159653, - 164658, - 168228, - 158881, - 162209, - 163708, - 161069, - 163570, - 163142, - 157970, - 162077, - 166069, - 164428, - 158002, - 163666, - 163814, - 164500, - 160990, - 153709, - 166111, - 160415, - 169103, - 167784, - 161829, - 156268, - 157418, - 165429, - 153859, - 162762, - 164066, - 163176, - 156498, - 162284, - 166037, - 163856, - 161545, - 165624, - 161354, - 159398, - 164989, - 163524, - 155831, - 156628, - 163887, - 162095, - 163942, - 164211, - 162113, - 162769, - 164309, - 168485, - 163780, - 160211, - 166651, - 166292, - 161578, - 161489, - 166747, - 163504, - 162649, - 161435, - 164385, - 161916, - 167290, - 166111, - 162310, - 159957, - 166471, - 164873, - 160245, - 156450, - 172877, - 164003, - 157906, - 162270, - 159396, - 160451, - 164356, - 166144, - 164564, - 160422, - 163559, - 164688, - 159573, - 157051, - 162575, - 161503, - 159521, - 169535, - 165828, - 167464, - 163472, - 163216, - 160606, - 159553, - 163831, - 162996, - 161945, - 161313, - 165726, - 166298, - 163297, - 160632, - 160230, - 161553, - 164482, - 164547, - 162121, - 158380, - 162396, - 162058, - 154843, - 161647, - 164470, - 161354, - 161555, - 164029, - 155643, - 156583, - 165344, - 167366, - 165488, - 157505, - 161374, - 159775, - 161039, - 158503, - 165802, - 165407, - 156947, - 164001, - 160697, - 162975, - 165617, - 161738, - 163459, - 157736, - 164121, - 166723, - 156272, - 160686, - 161788, - 164904, - 161509, - 160065, - 163469, - 161739, - 160339, - 165634, - 168834, - 161943, - 158335, - 167021, - 160755, - 157045, - 160742, - 163284, - 157754, - 161488, - 166142, - 162949, - 160795, - 164907, - 163645, - 161848, - 163174, - 162642 - ], - "p50Micros": 162396, - "p95Micros": 167366, - "minMicros": 153709, - "maxMicros": 172877, - "wallMicros": 32500941, - "operationsPerSecond": 6.153667981490136 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 171921, - 172154, - 172048, - 172525, - 340960, - 341162, - 341394, - 341569, - 332616, - 332449, - 332799, - 333480, - 331934, - 332484, - 332103, - 332768, - 338617, - 338515, - 338872, - 339452, - 335734, - 335013, - 335892, - 335924, - 341653, - 342379, - 342444, - 343014, - 346453, - 346435, - 346523, - 346553, - 333479, - 333443, - 334261, - 334066, - 326660, - 328076, - 327792, - 328297, - 336001, - 336402, - 336671, - 336561, - 341759, - 343460, - 343663, - 343784, - 341203, - 341143, - 341175, - 341839, - 338315, - 338421, - 340173, - 341551, - 333765, - 333420, - 333898, - 333575, - 329448, - 328622, - 329336, - 329177, - 331019, - 331224, - 331166, - 331906, - 335477, - 335488, - 335411, - 335807, - 337417, - 338672, - 338581, - 339762, - 338467, - 338043, - 338785, - 338838, - 341066, - 342881, - 342981, - 343858, - 332863, - 333618, - 333472, - 334173, - 322344, - 323230, - 324201, - 325272, - 334203, - 334160, - 334153, - 334896, - 337188, - 337413, - 337443, - 338402, - 338499, - 338431, - 338899, - 338632, - 339764, - 339791, - 340602, - 341526, - 341968, - 342168, - 342225, - 343579, - 337769, - 336926, - 336832, - 337792, - 327527, - 327305, - 326796, - 327363, - 332623, - 333230, - 333357, - 334040, - 342643, - 342316, - 343065, - 343116, - 341293, - 341461, - 341585, - 341647, - 340578, - 341524, - 341472, - 342456, - 342529, - 343380, - 342789, - 342570, - 331958, - 333092, - 332890, - 332803, - 327799, - 327671, - 327952, - 328744, - 330553, - 331132, - 331102, - 332052, - 335257, - 335731, - 336499, - 336549, - 341526, - 341343, - 342731, - 342403, - 334802, - 336329, - 336337, - 336325, - 332016, - 332863, - 331918, - 332539, - 328742, - 328198, - 328085, - 328958, - 326495, - 327747, - 327301, - 327794, - 333843, - 333812, - 334799, - 334529, - 331198, - 331858, - 332511, - 332184, - 333661, - 333754, - 334451, - 335540, - 341422, - 342024, - 341760, - 342715, - 342333, - 342553, - 344323, - 343922, - 330099, - 329370, - 329675, - 329959 - ], - "p50Micros": 335734, - "p95Micros": 343460, - "minMicros": 171921, - "maxMicros": 346553, - "wallMicros": 8399807, - "operationsPerSecond": 23.81007087424747 - }, - "orm": { - "samplesMicros": [ - 162115, - 162551, - 162866, - 163184, - 327421, - 328030, - 328402, - 328754, - 334668, - 336104, - 336048, - 335823, - 337668, - 339815, - 339267, - 339969, - 338618, - 340331, - 340003, - 340077, - 334117, - 333718, - 335505, - 334464, - 324076, - 323357, - 324093, - 326137, - 329353, - 329748, - 330569, - 331461, - 339296, - 340367, - 341254, - 340842, - 340329, - 340445, - 341790, - 342785, - 336281, - 337512, - 337123, - 336576, - 328874, - 328552, - 330336, - 329557, - 326361, - 326829, - 327316, - 326843, - 327506, - 327043, - 327676, - 328271, - 335307, - 335536, - 336520, - 336628, - 344246, - 346055, - 345173, - 346035, - 342974, - 342811, - 343484, - 343057, - 340788, - 340730, - 342551, - 341704, - 343880, - 343966, - 345457, - 345181, - 331680, - 333274, - 332556, - 333118, - 323756, - 324796, - 324828, - 325806, - 333303, - 333537, - 334270, - 334400, - 340522, - 341344, - 342073, - 341343, - 340336, - 340929, - 340977, - 342916, - 337811, - 339000, - 338739, - 338440, - 339478, - 339940, - 340880, - 342102, - 333209, - 333081, - 333024, - 332858, - 328310, - 328312, - 329952, - 329236, - 338233, - 339056, - 339143, - 339879, - 341764, - 341962, - 343231, - 342465, - 346000, - 345501, - 346078, - 346515, - 347785, - 347901, - 349156, - 348102, - 334506, - 335384, - 336236, - 335705, - 327497, - 327641, - 327727, - 329098, - 329118, - 330131, - 329950, - 331352, - 335602, - 336743, - 336583, - 337970, - 341141, - 342484, - 341405, - 342226, - 346640, - 345658, - 346795, - 346663, - 348978, - 348668, - 349444, - 350888, - 334342, - 335562, - 334490, - 335348, - 324643, - 324408, - 325950, - 325381, - 334430, - 334616, - 334601, - 335197, - 343687, - 343573, - 345001, - 344595, - 342350, - 343247, - 342724, - 342544, - 332461, - 334052, - 333818, - 333821, - 333927, - 335238, - 335044, - 334963, - 337898, - 337803, - 337390, - 337926, - 330767, - 331607, - 331713, - 332548, - 330773, - 331377, - 331926, - 332472, - 335461, - 335954, - 335646, - 336914 - ], - "p50Micros": 336048, - "p95Micros": 346640, - "minMicros": 162115, - "maxMicros": 350888, - "wallMicros": 8411642, - "operationsPerSecond": 23.77657061486925 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 149, - 144, - 147, - 149, - 164655, - 165036, - 165367, - 165707, - 165374, - 165062, - 164870, - 164750, - 164739, - 165070, - 165239, - 165265, - 159660, - 159239, - 158967, - 158825, - 167888, - 168063, - 168320, - 168540, - 171381, - 171291, - 171276, - 171455, - 171309, - 171186, - 171114, - 170874, - 169406, - 169279, - 169229, - 169208, - 170623, - 170456, - 170411, - 170376, - 164821, - 164782, - 164748, - 164732, - 165654, - 165480, - 165473, - 165451, - 165891, - 166003, - 166076, - 166170, - 168940, - 168980, - 168896, - 168885, - 172291, - 172247, - 172327, - 172334, - 170506, - 170403, - 170407, - 170604 - ], - "p50Micros": 166170, - "p95Micros": 172247, - "minMicros": 144, - "maxMicros": 172334, - "wallMicros": 10052200 - }, - "sqlMicros": [ - 79623, - 79841, - 80034, - 80129, - 83962, - 84704, - 84788, - 85579, - 81497, - 82019, - 82435, - 82900, - 82218, - 82556, - 82839, - 82678, - 82991, - 82628, - 83071, - 82998, - 81054, - 81421, - 82008, - 82523, - 78210, - 78911, - 79510, - 78684, - 79862, - 80063, - 80258, - 80458, - 80271, - 80614, - 80787, - 80977, - 86452, - 87197, - 87383, - 87586, - 82855, - 83350, - 84152, - 83829, - 86673, - 87266, - 87463, - 88087, - 81920, - 82761, - 81596, - 82503, - 87863, - 88471, - 88927, - 89086, - 81792, - 82350, - 82709, - 83122, - 86192, - 86374, - 86496, - 86795, - 81446, - 82404, - 81953, - 82262, - 87999, - 88042, - 88263, - 88539, - 81915, - 81955, - 82280, - 82750, - 82295, - 82018, - 82281, - 82571, - 81057, - 81724, - 81549, - 81415, - 83672, - 83900, - 83904, - 84232, - 80979, - 80689, - 81086, - 81010, - 84406, - 84589, - 84928, - 85367, - 82286, - 82692, - 83046, - 82362, - 85662, - 86035, - 86166, - 86601, - 82577, - 83008, - 83251, - 83688, - 87799, - 88672, - 89063, - 89489, - 81034, - 82098, - 81802, - 81610, - 88017, - 88329, - 88844, - 88810, - 80348, - 80135, - 79481, - 79977, - 85001, - 85298, - 85827, - 86000 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 170, - 228, - 248, - 264, - 159743, - 160079, - 160309, - 160528, - 163872, - 163842, - 163856, - 163861, - 166556, - 166882, - 166910, - 166971, - 175931, - 175448, - 175436, - 175416, - 167676, - 167663, - 167822, - 167905, - 171077, - 170947, - 170797, - 170701, - 167765, - 168000, - 168074, - 168226, - 164460, - 163916, - 163688, - 163438, - 166115, - 166257, - 166370, - 166489, - 165020, - 164856, - 164798, - 164767, - 166338, - 166413, - 166469, - 166523, - 173283, - 173204, - 173175, - 173192, - 167073, - 167042, - 167104, - 167140, - 170550, - 170305, - 170109, - 169988, - 161044, - 161025, - 161083, - 161080 - ], - "p50Micros": 166523, - "p95Micros": 175416, - "minMicros": 170, - "maxMicros": 175931, - "wallMicros": 10025517 - }, - "sqlMicros": [ - 79050, - 79344, - 79418, - 79589, - 79610, - 80253, - 80652, - 80639, - 78985, - 79447, - 79967, - 79303, - 83336, - 84229, - 84058, - 84468, - 81020, - 81097, - 80773, - 81135, - 84116, - 85334, - 85927, - 85841, - 84401, - 85312, - 86180, - 85356, - 88792, - 89954, - 90042, - 89990, - 80907, - 80707, - 81144, - 81929, - 85110, - 85810, - 86630, - 86781, - 81492, - 80768, - 81685, - 81452, - 88589, - 89242, - 88948, - 89382, - 80037, - 80986, - 80254, - 80919, - 85876, - 86590, - 87520, - 87631, - 81040, - 80684, - 80436, - 80105, - 82573, - 83130, - 83110, - 83172, - 81294, - 81380, - 81788, - 81428, - 84083, - 84300, - 84680, - 84755, - 81732, - 81634, - 82055, - 82772, - 82080, - 82681, - 82322, - 82672, - 78795, - 79481, - 79431, - 79088, - 86152, - 86534, - 87233, - 87334, - 81046, - 81851, - 82697, - 82596, - 89741, - 90018, - 89884, - 92253, - 80757, - 81355, - 81776, - 81085, - 84358, - 85455, - 85599, - 85793, - 82959, - 83522, - 83362, - 83967, - 86556, - 85943, - 85826, - 86631, - 79552, - 79425, - 79749, - 80179, - 80685, - 80974, - 81079, - 81204, - 79173, - 79191, - 78883, - 79224, - 83254, - 83728, - 84082, - 83980 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 108 - ], - "sqlMicros": [ - 81587, - 78399 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 219 - ], - "sqlMicros": [ - 80707, - 79474 - ], - "ormDecodeMicros": [ - 242, - 35 - ], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 80667, - 81805, - 80150, - 81642, - 80319, - 80232, - 79796, - 81041, - 80448, - 81614, - 81631, - 81380, - 77320, - 80992, - 81816, - 81472, - 78546, - 78850, - 79904, - 78725, - 79034, - 78514, - 80670, - 80041, - 80591, - 77262, - 77059, - 81030, - 81270, - 78705, - 77952, - 81445, - 77231, - 78432, - 76360, - 78464, - 78683, - 79614, - 78958, - 75740, - 78681, - 75270, - 79350, - 79804, - 77389, - 81679, - 80677, - 78757, - 75917, - 76021, - 79799, - 79950, - 75395, - 80829, - 81403, - 79392, - 75292, - 79282, - 81393, - 76651, - 77358, - 77021, - 78951, - 80078, - 77447, - 81642, - 80675, - 78418, - 78716, - 77635, - 78648, - 80324, - 80484, - 77566, - 80938, - 81064, - 76476, - 79455, - 80528, - 77826, - 79763, - 79771, - 80792, - 77034, - 79911, - 80714, - 80539, - 78639, - 78691, - 80023, - 78462, - 77604, - 77242, - 77854, - 82138, - 80478, - 79325, - 80052, - 77887, - 79144, - 81630, - 78916, - 80850, - 77446, - 79175, - 80168, - 76323, - 78925, - 79578, - 77190, - 79604, - 77480, - 79398, - 77270, - 78612, - 82798, - 79536, - 78929, - 80269, - 77541, - 80613, - 78506, - 80972, - 75882, - 77111, - 80284, - 81612, - 76814, - 78583, - 79048, - 79215, - 77888, - 79683, - 79674, - 80365, - 80508, - 81859, - 80724, - 79563, - 81684, - 80449, - 78589, - 80028, - 81341, - 80018, - 78710, - 80556, - 79229, - 77494, - 82335, - 79062, - 78775, - 81310, - 80942, - 76870, - 77965, - 81224, - 78646, - 80321, - 78324, - 79962, - 81715, - 78598, - 78100, - 79853, - 82146, - 79013, - 79676, - 80907, - 80244, - 79600, - 79760, - 81672, - 80233, - 80602, - 80879, - 79950, - 77592, - 79000, - 79982, - 81689, - 76775, - 79227, - 78061, - 78005, - 81538, - 79039, - 80636, - 81355, - 81258, - 80129, - 79760, - 77693, - 80203, - 80553, - 80419, - 76862, - 77563, - 80266, - 80069 - ], - "p50Micros": 79614, - "p95Micros": 81679, - "minMicros": 75270, - "maxMicros": 82798, - "wallMicros": 15880647, - "operationsPerSecond": 12.593945322252928 - }, - "orm": { - "samplesMicros": [ - 82103, - 80564, - 79523, - 81554, - 79025, - 78134, - 79356, - 80423, - 79917, - 81572, - 83845, - 80283, - 77627, - 81916, - 82529, - 79493, - 81797, - 78299, - 78352, - 77071, - 76092, - 79386, - 79281, - 81836, - 79972, - 77580, - 76781, - 78782, - 78045, - 80864, - 80386, - 81561, - 79740, - 79686, - 81109, - 78844, - 81543, - 80145, - 79481, - 81619, - 78406, - 80347, - 76415, - 76922, - 79314, - 79052, - 79958, - 79276, - 75352, - 78235, - 78105, - 77147, - 78661, - 76825, - 74425, - 75449, - 80333, - 76113, - 80813, - 79432, - 79769, - 76049, - 76480, - 77360, - 80341, - 80267, - 80800, - 76250, - 80458, - 79271, - 79679, - 77608, - 79219, - 80124, - 82018, - 80873, - 79813, - 78799, - 80284, - 78158, - 79781, - 80110, - 79279, - 81531, - 75444, - 77223, - 79694, - 81340, - 79980, - 83314, - 81532, - 78367, - 80739, - 83586, - 80079, - 78634, - 81316, - 80628, - 77601, - 81227, - 82029, - 77172, - 78521, - 80124, - 80671, - 80932, - 81267, - 80127, - 82333, - 79266, - 79884, - 78674, - 74549, - 77189, - 77154, - 80959, - 78972, - 79927, - 83036, - 78275, - 79793, - 81195, - 78913, - 80260, - 82294, - 80204, - 79313, - 78633, - 82169, - 78272, - 81504, - 82396, - 81158, - 80587, - 78645, - 80758, - 80570, - 77259, - 81767, - 79420, - 79244, - 77046, - 80767, - 80134, - 79988, - 81173, - 79836, - 80296, - 79197, - 81143, - 77851, - 79982, - 79135, - 81218, - 79444, - 78035, - 80538, - 79665, - 79843, - 78378, - 80229, - 79896, - 78539, - 75052, - 80261, - 79922, - 80273, - 79251, - 82811, - 79960, - 80294, - 82965, - 81369, - 80089, - 82250, - 82137, - 78011, - 76235, - 81852, - 80038, - 81778, - 78069, - 83442, - 79466, - 78675, - 79971, - 79120, - 79312, - 81629, - 79227, - 80047, - 78088, - 78401, - 77640, - 81337, - 80931, - 78912, - 83182, - 79638, - 76501 - ], - "p50Micros": 79836, - "p95Micros": 82333, - "minMicros": 74425, - "maxMicros": 83845, - "wallMicros": 15927605, - "operationsPerSecond": 12.556815666887772 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 81950, - 82480, - 82537, - 82595, - 165523, - 165658, - 165710, - 165763, - 165823, - 166152, - 166658, - 166119, - 162103, - 162193, - 162203, - 162219, - 159322, - 159256, - 159511, - 159489, - 160498, - 161016, - 160783, - 160916, - 160303, - 160381, - 160394, - 160910, - 162064, - 162461, - 162755, - 163059, - 163252, - 163493, - 163616, - 163853, - 160887, - 160313, - 160737, - 160552, - 158741, - 158919, - 159144, - 158904, - 156431, - 156625, - 156599, - 156875, - 155547, - 155668, - 155828, - 156019, - 162539, - 163047, - 163151, - 163511, - 166714, - 167259, - 167193, - 167196, - 163329, - 163692, - 163936, - 164391, - 162136, - 162240, - 162720, - 162534, - 159686, - 160037, - 159813, - 159798, - 162016, - 162461, - 162383, - 162316, - 161405, - 161802, - 161648, - 161762, - 158465, - 158730, - 159093, - 158956, - 159301, - 159792, - 159721, - 159675, - 157150, - 157583, - 157450, - 157376, - 158096, - 158313, - 158765, - 158391, - 159395, - 159420, - 159576, - 159775, - 156029, - 156338, - 156272, - 156727, - 156724, - 157078, - 157222, - 157129, - 160522, - 160400, - 160418, - 160583, - 160509, - 160383, - 160402, - 160599, - 158923, - 159281, - 159152, - 159165, - 159139, - 159431, - 159849, - 159549, - 159848, - 160373, - 160788, - 160683, - 161373, - 161665, - 162242, - 161970, - 160991, - 161916, - 161414, - 161614, - 157172, - 157502, - 157492, - 157867, - 159365, - 159484, - 159940, - 159766, - 162873, - 163107, - 163422, - 163232, - 162298, - 162403, - 162766, - 162620, - 159887, - 159662, - 159619, - 159550, - 157574, - 157917, - 157926, - 158194, - 157070, - 157118, - 157153, - 157294, - 156937, - 157096, - 157467, - 157173, - 161399, - 161837, - 161743, - 161671, - 164958, - 165008, - 165349, - 165064, - 162639, - 162700, - 163104, - 162904, - 158679, - 158759, - 159078, - 158972, - 159413, - 160100, - 160646, - 160647, - 164384, - 164448, - 164608, - 164968, - 164132, - 165135, - 164212, - 164517, - 159946, - 159692, - 159816, - 159692 - ], - "p50Micros": 160383, - "p95Micros": 165658, - "minMicros": 81950, - "maxMicros": 167259, - "wallMicros": 4018563, - "operationsPerSecond": 49.7690343538225 - }, - "orm": { - "samplesMicros": [ - 82677, - 83011, - 83150, - 83301, - 163291, - 163568, - 163623, - 163659, - 159037, - 158735, - 158693, - 158947, - 156716, - 156867, - 156888, - 157324, - 158504, - 158673, - 159016, - 158893, - 160001, - 160214, - 160261, - 160291, - 156637, - 158527, - 158975, - 158669, - 155885, - 156559, - 156023, - 156202, - 156128, - 156766, - 158963, - 157287, - 158289, - 159334, - 159245, - 159265, - 160914, - 160969, - 160893, - 160895, - 162804, - 164306, - 163719, - 164010, - 166270, - 166089, - 166122, - 166122, - 164341, - 164016, - 163944, - 163901, - 166268, - 166026, - 166011, - 166401, - 167303, - 167677, - 167847, - 168184, - 161170, - 161860, - 161591, - 161822, - 156594, - 156803, - 157225, - 156881, - 156101, - 156477, - 156879, - 156753, - 156498, - 157422, - 157250, - 157267, - 156157, - 156386, - 156625, - 156948, - 158870, - 159802, - 159322, - 159322, - 162672, - 163454, - 163885, - 164469, - 162779, - 163209, - 163863, - 163531, - 162419, - 163703, - 164975, - 163391, - 160917, - 161671, - 161245, - 161448, - 159951, - 160126, - 160805, - 160521, - 160862, - 160761, - 161078, - 160998, - 160341, - 161301, - 160909, - 161145, - 163280, - 163969, - 164657, - 164150, - 162576, - 162969, - 162927, - 163373, - 160133, - 160115, - 160797, - 160131, - 159050, - 159122, - 159163, - 159362, - 161812, - 162289, - 162017, - 162151, - 161932, - 162374, - 162397, - 162897, - 162103, - 162228, - 162659, - 162337, - 162901, - 163177, - 163765, - 163606, - 163675, - 164264, - 164315, - 164445, - 162483, - 163320, - 163068, - 162942, - 160604, - 161205, - 160707, - 161738, - 159842, - 160673, - 160393, - 160609, - 157806, - 158333, - 158057, - 158230, - 160603, - 161210, - 161767, - 162432, - 160817, - 161086, - 161667, - 161387, - 155509, - 154768, - 154990, - 154693, - 152290, - 152534, - 153070, - 152432, - 153050, - 153475, - 153513, - 153624, - 157782, - 158170, - 158559, - 158547, - 162042, - 161844, - 161986, - 162301, - 161917, - 161645, - 162350, - 161896 - ], - "p50Micros": 160893, - "p95Micros": 166026, - "minMicros": 82677, - "maxMicros": 168184, - "wallMicros": 4016613, - "operationsPerSecond": 49.793196407022535 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 143, - 131, - 133, - 135, - 81174, - 81489, - 81566, - 81687, - 79372, - 79034, - 78994, - 78970, - 80562, - 80496, - 80545, - 80575, - 81570, - 81561, - 81526, - 81577, - 79609, - 79252, - 79183, - 79134, - 78777, - 78702, - 78695, - 78703, - 82270, - 82184, - 82196, - 82206, - 79830, - 79730, - 79702, - 79689, - 76564, - 76451, - 76429, - 76408, - 78026, - 77979, - 77978, - 77985, - 77020, - 76974, - 76979, - 76993, - 80126, - 80098, - 80111, - 80139, - 81118, - 81094, - 81142, - 81181, - 81684, - 81602, - 81561, - 81544, - 81271, - 81383, - 81407, - 81467 - ], - "p50Micros": 79830, - "p95Micros": 82184, - "minMicros": 131, - "maxMicros": 82270, - "wallMicros": 4793816 - }, - "sqlMicros": [ - 80846, - 81327, - 81469, - 81507, - 78792, - 78891, - 78831, - 79095, - 79891, - 80499, - 80559, - 80585, - 80884, - 81227, - 81890, - 81601, - 79257, - 79160, - 79002, - 79211, - 78316, - 78769, - 78728, - 78644, - 81814, - 82260, - 82219, - 82145, - 79289, - 79685, - 79660, - 79933, - 76207, - 76297, - 76566, - 76503, - 77780, - 78024, - 77916, - 77995, - 76691, - 76939, - 77073, - 77001, - 79775, - 80055, - 80078, - 80278, - 80912, - 80897, - 81146, - 81189, - 81096, - 81675, - 81508, - 81699, - 80791, - 81211, - 81574, - 81564, - 81683, - 82201, - 81705, - 81994 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 184, - 280, - 302, - 319, - 82689, - 82948, - 83097, - 83252, - 81109, - 80999, - 80939, - 80919, - 82539, - 82240, - 82268, - 82332, - 84221, - 84183, - 84163, - 84149, - 81572, - 81415, - 81392, - 81366, - 82883, - 82829, - 82808, - 82806, - 77615, - 77613, - 77574, - 77556, - 80648, - 80189, - 80164, - 80160, - 78923, - 78926, - 78943, - 78956, - 79512, - 79475, - 79480, - 79503, - 80244, - 80194, - 80191, - 80200, - 81303, - 81385, - 81432, - 81480, - 81431, - 81347, - 81509, - 81540, - 83715, - 83632, - 83512, - 83418, - 81030, - 80929, - 80905, - 80885 - ], - "p50Micros": 81109, - "p95Micros": 84149, - "minMicros": 184, - "maxMicros": 84221, - "wallMicros": 4875722 - }, - "sqlMicros": [ - 82240, - 82614, - 82858, - 82869, - 80220, - 80761, - 81184, - 80767, - 81762, - 81962, - 82194, - 82561, - 83301, - 84093, - 84372, - 84217, - 80851, - 81320, - 81625, - 81263, - 82501, - 82551, - 82847, - 82824, - 76809, - 77546, - 77766, - 77506, - 79964, - 80076, - 80074, - 80333, - 78584, - 78951, - 78953, - 78913, - 79033, - 79406, - 79592, - 79505, - 79722, - 80130, - 80142, - 80394, - 80704, - 81397, - 81370, - 81557, - 81051, - 81111, - 81350, - 81439, - 82722, - 83546, - 83357, - 83923, - 80369, - 80989, - 80848, - 81032, - 79666, - 79486, - 79423, - 79623 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 55 - ], - "sqlMicros": [ - 79175 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 126 - ], - "sqlMicros": [ - 82060 - ], - "ormDecodeMicros": [ - 9 - ], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - } - } - } - ] - } - ] -} diff --git a/research/benchmarks/runtime.json b/research/benchmarks/runtime.json deleted file mode 100644 index 5c63d6e9..00000000 --- a/research/benchmarks/runtime.json +++ /dev/null @@ -1,17876 +0,0 @@ -{ - "recordedAt": "2026-09-15T06:41:48.294527Z", - "sourceCommit": "94d24fed7b008f2444830f9088e73ee830aa092a", - "harnessSha256": { - "tool/benchmark_runtime.dart": "4613bf4cd128018db02d907c5cf34d77c4235bc6f010c9534e92594431912366", - "tool/src/runtime_cases.dart": "fbd025c22388055452994d476d2ca1118bf123537d7c5f09a35c887d8887608c", - "tool/src/runtime_relay.dart": "cb6eb51792a6993c0143eea08f9f5bb9a6ebb31a3da32821047371709cde44cc", - "tool/src/runtime_vm.dart": "9a1f4f87be3371f9cee2d9a093def47cdf4679764d69ac219ba218a3a0931ca5" - }, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "cpu": "Apple M3 Max", - "samplesPerLane": 200, - "warmupPairs": 20, - "smoke": false, - "scope": "Warm native JIT, reused query objects; raw uses the same public Driver and precompiled identical SQL with manual same-codec mapping. ORM includes compilation. Default hooks and allocation tracing are disabled in timing. Loopback relay has no loss or bandwidth limit; delayed scenario is controlled TCP latency, not a remote-server measurement. PostgreSQL protocol bytes exclude TCP/IP headers. SQLite byte counts are UTF-8 JSON of driver rows, not IPC wire bytes. Heap fields are live isolate-group memory; traces count selected classes only, not total allocated bytes.", - "fixture": { - "users": 100, - "postsPerUser": 10, - "selectedPostsPerUser": 3, - "nullableNicknames": true, - "sqlite": "temporary file, WAL, warm cache", - "postgresPool": 4, - "concurrentClients": 8 - }, - "results": [ - { - "scenario": "sqlite_wal", - "environment": { - "backend": "sqlite_wal", - "databaseVersion": "3.53.4", - "pid": 94368 - }, - "oneWayDelayMs": 0, - "selectOneMicros": { - "samplesMicros": [ - 401, - 59, - 45, - 40, - 36, - 36, - 37, - 40, - 34, - 36 - ], - "p50Micros": 37, - "p95Micros": 401, - "minMicros": 34, - "maxMicros": 401, - "wallMicros": 764, - "operationsPerSecond": 13089.005235602095 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 229, - 169, - 171, - 173, - 167, - 176, - 167, - 264, - 173, - 171, - 166, - 158, - 148, - 157, - 146, - 162, - 158, - 155, - 153, - 158, - 159, - 169, - 182, - 167, - 156, - 159, - 131, - 136, - 132, - 127, - 125, - 128, - 124, - 130, - 124, - 127, - 125, - 137, - 130, - 126, - 132, - 126, - 128, - 126, - 124, - 127, - 123, - 128, - 123, - 130, - 129, - 269, - 150, - 146, - 140, - 121, - 155, - 116, - 111, - 115, - 110, - 113, - 106, - 112, - 188, - 114, - 207, - 121, - 151, - 121, - 175, - 117, - 107, - 111, - 101, - 110, - 105, - 112, - 104, - 114, - 105, - 113, - 116, - 120, - 114, - 109, - 101, - 109, - 102, - 131, - 116, - 115, - 104, - 111, - 101, - 163, - 106, - 108, - 142, - 110, - 101, - 107, - 166, - 111, - 103, - 109, - 101, - 109, - 100, - 102, - 100, - 108, - 100, - 109, - 101, - 108, - 100, - 108, - 100, - 109, - 101, - 109, - 101, - 109, - 101, - 123, - 107, - 111, - 102, - 109, - 101, - 110, - 194, - 116, - 105, - 111, - 102, - 110, - 99, - 110, - 108, - 107, - 104, - 109, - 128, - 112, - 187, - 111, - 101, - 110, - 100, - 115, - 103, - 113, - 99, - 105, - 100, - 102, - 101, - 109, - 113, - 109, - 117, - 102, - 216, - 112, - 102, - 110, - 102, - 112, - 102, - 108, - 100, - 107, - 98, - 106, - 101, - 102, - 100, - 109, - 99, - 101, - 100, - 118, - 103, - 109, - 100, - 110, - 155, - 100, - 99, - 108, - 99, - 108, - 98, - 109, - 107, - 102, - 101, - 108 - ], - "p50Micros": 111, - "p95Micros": 175, - "minMicros": 98, - "maxMicros": 269, - "wallMicros": 24698, - "operationsPerSecond": 8097.821685966475 - }, - "orm": { - "samplesMicros": [ - 254, - 234, - 212, - 215, - 216, - 209, - 212, - 204, - 219, - 231, - 206, - 192, - 192, - 189, - 197, - 198, - 224, - 288, - 287, - 205, - 226, - 242, - 236, - 233, - 224, - 296, - 205, - 197, - 200, - 188, - 185, - 186, - 184, - 186, - 188, - 180, - 458, - 203, - 190, - 188, - 186, - 182, - 194, - 179, - 183, - 180, - 182, - 179, - 183, - 180, - 440, - 186, - 250, - 226, - 202, - 185, - 180, - 165, - 163, - 154, - 160, - 155, - 161, - 155, - 166, - 162, - 171, - 165, - 216, - 194, - 2614, - 166, - 160, - 152, - 152, - 147, - 154, - 147, - 153, - 194, - 154, - 168, - 220, - 189, - 163, - 150, - 153, - 147, - 151, - 190, - 162, - 171, - 153, - 154, - 149, - 143, - 148, - 146, - 175, - 148, - 159, - 166, - 164, - 149, - 150, - 142, - 176, - 149, - 147, - 143, - 144, - 143, - 148, - 147, - 144, - 143, - 144, - 141, - 143, - 139, - 143, - 143, - 143, - 141, - 180, - 231, - 161, - 149, - 149, - 143, - 149, - 162, - 187, - 156, - 160, - 148, - 144, - 140, - 146, - 138, - 205, - 155, - 146, - 170, - 176, - 160, - 140, - 137, - 143, - 134, - 229, - 156, - 145, - 138, - 355, - 208, - 152, - 139, - 162, - 161, - 167, - 163, - 153, - 267, - 160, - 146, - 142, - 135, - 135, - 138, - 142, - 135, - 139, - 135, - 137, - 131, - 143, - 137, - 143, - 131, - 135, - 136, - 140, - 223, - 198, - 137, - 139, - 130, - 138, - 134, - 138, - 129, - 138, - 129, - 134, - 130, - 139, - 141, - 135, - 133 - ], - "p50Micros": 160, - "p95Micros": 242, - "minMicros": 129, - "maxMicros": 2614, - "wallMicros": 37076, - "operationsPerSecond": 5394.325169921243 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 152, - 256, - 359, - 461, - 649, - 755, - 854, - 952, - 905, - 897, - 893, - 888, - 799, - 790, - 790, - 791, - 791, - 793, - 807, - 810, - 812, - 812, - 812, - 812, - 813, - 812, - 797, - 797, - 795, - 795, - 797, - 939, - 944, - 942, - 940, - 938, - 939, - 939, - 936, - 792, - 788, - 790, - 793, - 844, - 846, - 922, - 946, - 951, - 953, - 952, - 951, - 900, - 898, - 822, - 798, - 792, - 807, - 813, - 901, - 907, - 905, - 904, - 908, - 930, - 929, - 935, - 856, - 865, - 875, - 943, - 943, - 923, - 906, - 901, - 892, - 877, - 867, - 801, - 866, - 880, - 980, - 986, - 993, - 1070, - 1077, - 1078, - 1009, - 993, - 892, - 877, - 871, - 972, - 991, - 998, - 1014, - 1030, - 1046, - 1066, - 1082, - 924, - 918, - 1079, - 1083, - 1073, - 1068, - 1051, - 1040, - 1022, - 1001, - 828, - 808, - 801, - 789, - 794, - 847, - 848, - 848, - 849, - 852, - 851, - 851, - 847, - 788, - 784, - 784, - 783, - 780, - 779, - 780, - 776, - 773, - 775, - 775, - 774, - 775, - 776, - 776, - 777, - 778, - 776, - 775, - 776, - 775, - 776, - 777, - 778, - 779, - 780, - 785, - 808, - 878, - 883, - 883, - 882, - 880, - 970, - 979, - 970, - 919, - 940, - 947, - 947, - 947, - 855, - 844, - 832, - 812, - 787, - 783, - 783, - 783, - 783, - 782, - 780, - 786, - 786, - 781, - 780, - 785, - 785, - 785, - 791, - 783, - 784, - 785, - 786, - 782, - 782, - 788, - 786, - 786, - 788, - 787, - 813, - 814, - 812, - 806, - 800, - 800, - 793 - ], - "p50Micros": 814, - "p95Micros": 1046, - "minMicros": 152, - "maxMicros": 1083, - "wallMicros": 21823, - "operationsPerSecond": 9164.642808046556 - }, - "orm": { - "samplesMicros": [ - 315, - 432, - 582, - 727, - 881, - 1021, - 1170, - 1362, - 1249, - 1261, - 1390, - 1424, - 1424, - 1432, - 1430, - 1389, - 1374, - 1356, - 1210, - 1171, - 1153, - 1142, - 1134, - 1121, - 1123, - 1121, - 1124, - 1197, - 1195, - 1208, - 1202, - 1218, - 1212, - 1214, - 1217, - 1145, - 1240, - 1240, - 1267, - 1264, - 1280, - 1374, - 1372, - 1382, - 1290, - 1282, - 1257, - 1243, - 1231, - 1134, - 1127, - 1111, - 1109, - 1108, - 1126, - 1199, - 1241, - 1263, - 1290, - 1299, - 1303, - 1306, - 1391, - 1316, - 1275, - 1248, - 1224, - 1213, - 1208, - 1195, - 1089, - 1084, - 1074, - 1076, - 1128, - 1129, - 1151, - 1152, - 1160, - 1159, - 1168, - 1162, - 1109, - 1105, - 1082, - 1079, - 1071, - 1070, - 1064, - 1064, - 1059, - 1065, - 1063, - 1064, - 1075, - 1079, - 1083, - 1119, - 1120, - 1118, - 1118, - 1119, - 1105, - 1102, - 1098, - 1062, - 1060, - 1058, - 1057, - 1054, - 1055, - 1065, - 1078, - 1157, - 1219, - 1221, - 1241, - 1257, - 1296, - 1281, - 1298, - 1233, - 1175, - 1187, - 1179, - 1173, - 1133, - 1139, - 1110, - 1094, - 1096, - 1082, - 1076, - 1065, - 1067, - 1072, - 1070, - 1088, - 1082, - 1075, - 1064, - 1061, - 1069, - 1064, - 1090, - 1080, - 1093, - 1099, - 1111, - 1118, - 1112, - 1109, - 1089, - 1085, - 1075, - 1069, - 1064, - 1056, - 1052, - 1053, - 1040, - 1035, - 1028, - 1031, - 1036, - 1044, - 1148, - 1162, - 1171, - 1184, - 1194, - 1199, - 1192, - 1194, - 1091, - 1080, - 1078, - 1067, - 1081, - 1073, - 1078, - 1073, - 1077, - 1073, - 1073, - 1077, - 1067, - 1072, - 1066, - 1066, - 1064, - 1062, - 1062, - 1054, - 1050, - 1096, - 1097, - 1095, - 1089, - 1064 - ], - "p50Micros": 1111, - "p95Micros": 1372, - "minMicros": 315, - "maxMicros": 1432, - "wallMicros": 28905, - "operationsPerSecond": 6919.2181283514965 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 22, - 162, - 277, - 392, - 508, - 623, - 738, - 853, - 805, - 806, - 807, - 807, - 808, - 808, - 840, - 844, - 845, - 840, - 1196, - 1203, - 1201, - 1169, - 1167, - 1168, - 1173, - 817, - 813, - 811, - 811, - 810, - 809, - 808, - 809, - 815, - 815, - 823, - 823, - 871, - 936, - 923, - 899, - 884, - 862, - 847, - 784, - 704, - 701, - 699, - 699, - 702, - 701, - 722, - 737, - 753, - 769, - 788, - 806, - 822, - 818, - 822, - 825, - 824, - 815, - 814 - ], - "p50Micros": 811, - "p95Micros": 1173, - "minMicros": 22, - "maxMicros": 1203, - "wallMicros": 51353 - }, - "sqlMicros": [ - 124, - 104, - 105, - 105, - 105, - 105, - 104, - 105, - 104, - 106, - 105, - 106, - 105, - 133, - 105, - 106, - 101, - 461, - 112, - 107, - 106, - 105, - 105, - 106, - 105, - 109, - 104, - 105, - 105, - 105, - 104, - 106, - 115, - 105, - 112, - 105, - 149, - 169, - 95, - 92, - 90, - 91, - 90, - 92, - 90, - 90, - 90, - 91, - 94, - 90, - 110, - 105, - 106, - 105, - 109, - 110, - 107, - 106, - 109, - 109, - 104, - 104, - 105, - 107 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 23, - 198, - 359, - 540, - 708, - 856, - 1015, - 1158, - 1070, - 1057, - 1014, - 993, - 979, - 1178, - 1174, - 1176, - 1167, - 1176, - 1169, - 1171, - 956, - 947, - 941, - 938, - 935, - 938, - 939, - 940, - 936, - 938, - 937, - 952, - 983, - 991, - 1014, - 1020, - 1014, - 1014, - 984, - 964, - 944, - 930, - 926, - 1191, - 1252, - 2248, - 2292, - 2327, - 2340, - 2097, - 2088, - 1044, - 1027, - 983, - 958, - 941, - 943, - 934, - 928, - 918, - 913, - 903, - 901, - 894 - ], - "p50Micros": 964, - "p95Micros": 2248, - "minMicros": 23, - "maxMicros": 2340, - "wallMicros": 68384 - }, - "sqlMicros": [ - 123, - 115, - 136, - 127, - 112, - 121, - 108, - 103, - 109, - 102, - 107, - 103, - 324, - 113, - 105, - 102, - 110, - 102, - 104, - 102, - 105, - 102, - 104, - 104, - 103, - 106, - 103, - 103, - 103, - 107, - 117, - 126, - 112, - 121, - 109, - 99, - 104, - 102, - 103, - 104, - 103, - 103, - 119, - 140, - 121, - 139, - 127, - 112, - 101, - 107, - 102, - 107, - 102, - 107, - 103, - 104, - 102, - 99, - 101, - 99, - 101, - 100, - 99, - 99 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 11 - ], - "sqlMicros": [ - 132 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 13 - ], - "sqlMicros": [ - 136 - ], - "ormDecodeMicros": [ - 52 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365686504708, - "endMicros": 1365686505876, - "retainedRootRows": 100, - "rssBefore": 308232192, - "rssAfter": 308969472, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14600544, - "heapCapacity": 28377088, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503076", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 5104 - }, - { - "name": "_Map", - "instancesCurrent": 14, - "bytesCurrent": 896 - }, - { - "name": "_List", - "instancesCurrent": 40105, - "bytesCurrent": 3101440 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1130, - "bytesCurrent": 36160 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14662864, - "heapCapacity": 26066944, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503220", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 9904 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40230, - "bytesCurrent": 3111184 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 342, - "counts": { - "_List": 33, - "_Record": 303, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_allocateData", - "samples": 21 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_worker", - "samples": 2 - }, - { - "function": "_List", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365686675414, - "endMicros": 1365686676658, - "retainedRootRows": 100, - "rssBefore": 315424768, - "rssAfter": 316555264, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14646096, - "heapCapacity": 26066944, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503251", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 5104 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40126, - "bytesCurrent": 3102576 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1128, - "bytesCurrent": 36096 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14663024, - "heapCapacity": 26591232, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503391", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 9904 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40230, - "bytesCurrent": 3111184 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 1569, - "counts": { - "_List": 807, - "_SelectionPlan": 3, - "_GrowableList": 432, - "_Map": 18, - "_Record": 309 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 765 - }, - { - "function": "", - "samples": 600 - }, - { - "function": "quote", - "samples": 48 - }, - { - "function": "_write", - "samples": 33 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "_List", - "samples": 17 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_addPart", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_plan", - "samples": 6 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 65, - 64, - 65, - 64, - 65, - 64, - 64, - 64, - 65, - 65, - 64, - 64, - 65, - 64, - 71, - 64, - 65, - 64, - 65, - 64, - 65, - 58, - 58, - 53, - 52, - 64, - 56, - 55, - 55, - 69, - 66, - 59, - 69, - 65, - 65, - 64, - 64, - 63, - 65, - 64, - 67, - 64, - 65, - 64, - 64, - 64, - 65, - 63, - 65, - 64, - 64, - 662, - 67, - 65, - 67, - 66, - 67, - 136, - 192, - 69, - 68, - 65, - 70, - 99, - 72, - 70, - 69, - 66, - 66, - 64, - 65, - 118, - 109, - 69, - 67, - 165, - 82, - 83, - 68, - 99, - 111, - 57, - 53, - 54, - 60, - 64, - 65, - 113, - 81, - 55, - 55, - 64, - 64, - 53, - 51, - 50, - 51, - 50, - 52, - 50, - 50, - 83, - 51, - 50, - 51, - 51, - 50, - 49, - 49, - 49, - 50, - 55, - 51, - 51, - 51, - 51, - 52, - 51, - 50, - 89, - 71, - 55, - 52, - 51, - 50, - 50, - 50, - 51, - 61, - 137, - 68, - 52, - 50, - 54, - 60, - 50, - 51, - 49, - 50, - 50, - 50, - 49, - 49, - 49, - 50, - 49, - 51, - 49, - 50, - 49, - 50, - 50, - 49, - 49, - 50, - 49, - 49, - 49, - 51, - 50, - 53, - 50, - 51, - 50, - 50, - 50, - 50, - 50, - 51, - 50, - 49, - 51, - 50, - 50, - 52, - 49, - 50, - 49, - 50, - 50, - 51, - 68, - 64, - 65, - 64, - 50, - 56, - 51, - 50, - 49, - 55, - 50, - 51, - 83, - 51, - 63, - 57, - 51, - 51, - 50 - ], - "p50Micros": 55, - "p95Micros": 99, - "minMicros": 49, - "maxMicros": 662, - "wallMicros": 12883, - "operationsPerSecond": 15524.33439416285 - }, - "orm": { - "samplesMicros": [ - 80, - 82, - 79, - 82, - 80, - 82, - 79, - 82, - 78, - 82, - 80, - 81, - 78, - 81, - 80, - 83, - 79, - 82, - 85, - 82, - 75, - 76, - 81, - 71, - 715, - 89, - 87, - 74, - 91, - 83, - 88, - 87, - 84, - 84, - 87, - 84, - 83, - 81, - 84, - 81, - 87, - 82, - 88, - 83, - 83, - 81, - 83, - 81, - 85, - 83, - 84, - 82, - 696, - 246, - 84, - 85, - 80, - 88, - 112, - 89, - 83, - 82, - 158, - 183, - 94, - 189, - 92, - 87, - 77, - 75, - 81, - 172, - 97, - 154, - 107, - 88, - 85, - 139, - 108, - 86, - 108, - 87, - 75, - 78, - 77, - 75, - 80, - 73, - 80, - 95, - 67, - 79, - 111, - 76, - 66, - 64, - 65, - 72, - 66, - 64, - 66, - 77, - 64, - 63, - 66, - 68, - 65, - 64, - 65, - 72, - 87, - 86, - 69, - 72, - 67, - 67, - 65, - 65, - 75, - 68, - 87, - 79, - 70, - 74, - 65, - 64, - 65, - 72, - 71, - 67, - 87, - 76, - 66, - 66, - 73, - 65, - 63, - 64, - 64, - 63, - 66, - 65, - 64, - 64, - 64, - 64, - 65, - 64, - 64, - 64, - 64, - 64, - 64, - 71, - 64, - 63, - 65, - 64, - 64, - 65, - 65, - 65, - 66, - 64, - 65, - 65, - 64, - 72, - 66, - 65, - 65, - 66, - 64, - 64, - 66, - 64, - 63, - 66, - 65, - 72, - 145, - 100, - 77, - 73, - 127, - 73, - 65, - 66, - 66, - 65, - 66, - 66, - 66, - 72, - 69, - 77, - 64, - 65, - 65, - 64 - ], - "p50Micros": 75, - "p95Micros": 127, - "minMicros": 63, - "maxMicros": 715, - "wallMicros": 17106, - "operationsPerSecond": 11691.804045364199 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 212, - 276, - 352, - 413, - 471, - 528, - 588, - 646, - 506, - 498, - 488, - 487, - 488, - 491, - 492, - 490, - 489, - 490, - 485, - 491, - 541, - 581, - 600, - 617, - 630, - 641, - 653, - 659, - 619, - 668, - 684, - 678, - 689, - 698, - 683, - 663, - 648, - 553, - 512, - 500, - 474, - 449, - 447, - 449, - 449, - 462, - 461, - 463, - 465, - 477, - 488, - 563, - 578, - 622, - 632, - 638, - 679, - 664, - 648, - 571, - 554, - 495, - 483, - 470, - 422, - 422, - 424, - 424, - 423, - 423, - 422, - 424, - 424, - 424, - 424, - 424, - 424, - 424, - 425, - 425, - 424, - 500, - 498, - 498, - 504, - 545, - 558, - 561, - 558, - 483, - 491, - 489, - 483, - 446, - 437, - 435, - 439, - 436, - 430, - 432, - 431, - 427, - 423, - 425, - 425, - 434, - 444, - 453, - 463, - 472, - 482, - 483, - 491, - 528, - 522, - 513, - 503, - 494, - 483, - 480, - 469, - 428, - 423, - 423, - 423, - 422, - 422, - 422, - 423, - 422, - 423, - 424, - 421, - 423, - 424, - 425, - 425, - 422, - 419, - 418, - 417, - 416, - 414, - 414, - 414, - 417, - 420, - 420, - 423, - 423, - 423, - 423, - 422, - 422, - 422, - 432, - 431, - 430, - 432, - 432, - 433, - 433, - 433, - 422, - 424, - 425, - 424, - 424, - 424, - 424, - 423, - 424, - 424, - 429, - 429, - 426, - 429, - 430, - 432, - 433, - 431, - 424, - 425, - 435, - 443, - 447, - 447, - 447, - 459, - 479, - 488, - 515, - 501, - 496, - 497, - 495, - 484, - 467, - 457, - 417 - ], - "p50Micros": 446, - "p95Micros": 653, - "minMicros": 212, - "maxMicros": 698, - "wallMicros": 12102, - "operationsPerSecond": 16526.194017517766 - }, - "orm": { - "samplesMicros": [ - 112, - 188, - 264, - 341, - 428, - 585, - 667, - 745, - 744, - 763, - 773, - 778, - 772, - 699, - 698, - 726, - 703, - 686, - 671, - 670, - 668, - 695, - 686, - 664, - 663, - 645, - 638, - 631, - 623, - 578, - 572, - 553, - 553, - 558, - 556, - 545, - 538, - 534, - 532, - 531, - 535, - 545, - 562, - 565, - 566, - 578, - 583, - 582, - 571, - 559, - 540, - 537, - 533, - 520, - 514, - 511, - 508, - 510, - 522, - 533, - 544, - 563, - 575, - 587, - 597, - 603, - 641, - 640, - 629, - 609, - 596, - 590, - 579, - 568, - 518, - 520, - 531, - 543, - 554, - 559, - 569, - 618, - 633, - 621, - 615, - 604, - 593, - 582, - 571, - 524, - 507, - 506, - 501, - 502, - 503, - 502, - 503, - 507, - 507, - 506, - 506, - 570, - 587, - 586, - 585, - 580, - 580, - 580, - 580, - 516, - 498, - 505, - 507, - 507, - 507, - 506, - 506, - 506, - 506, - 500, - 500, - 500, - 500, - 500, - 500, - 500, - 500, - 499, - 500, - 584, - 593, - 595, - 600, - 599, - 599, - 598, - 598, - 514, - 508, - 506, - 505, - 535, - 536, - 536, - 534, - 533, - 531, - 532, - 528, - 498, - 498, - 498, - 498, - 515, - 526, - 526, - 527, - 527, - 527, - 527, - 527, - 510, - 502, - 501, - 519, - 562, - 572, - 574, - 577, - 576, - 576, - 575, - 557, - 514, - 504, - 518, - 527, - 526, - 526, - 525, - 525, - 525, - 525, - 509, - 499, - 500, - 501, - 501, - 501, - 501, - 500, - 500, - 500, - 499, - 502, - 541, - 554, - 564, - 564, - 605 - ], - "p50Micros": 535, - "p95Micros": 695, - "minMicros": 112, - "maxMicros": 778, - "wallMicros": 14125, - "operationsPerSecond": 14159.29203539823 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 19, - 83, - 137, - 187, - 250, - 305, - 356, - 407, - 374, - 370, - 370, - 353, - 354, - 352, - 352, - 353, - 353, - 353, - 354, - 350, - 349, - 348, - 348, - 348, - 348, - 352, - 366, - 381, - 394, - 407, - 409, - 426, - 434, - 433, - 432, - 433, - 518, - 528, - 516, - 503, - 489, - 476, - 463, - 360, - 350, - 347, - 347, - 348, - 348, - 347, - 348, - 348, - 347, - 347, - 347, - 346, - 346, - 346, - 345, - 345, - 344, - 343, - 343, - 342 - ], - "p50Micros": 350, - "p95Micros": 503, - "minMicros": 19, - "maxMicros": 528, - "wallMicros": 23017 - }, - "sqlMicros": [ - 61, - 49, - 46, - 58, - 50, - 47, - 46, - 45, - 45, - 45, - 45, - 49, - 45, - 46, - 46, - 45, - 45, - 46, - 46, - 45, - 45, - 46, - 45, - 45, - 50, - 59, - 59, - 58, - 59, - 47, - 61, - 59, - 58, - 58, - 58, - 144, - 57, - 49, - 45, - 45, - 45, - 45, - 45, - 45, - 46, - 45, - 45, - 45, - 45, - 45, - 46, - 45, - 45, - 44, - 45, - 45, - 45, - 44, - 45, - 44, - 44, - 44, - 44, - 44 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 20, - 94, - 169, - 246, - 308, - 370, - 432, - 495, - 458, - 441, - 431, - 431, - 430, - 430, - 430, - 430, - 430, - 430, - 429, - 429, - 430, - 429, - 428, - 427, - 428, - 428, - 428, - 428, - 427, - 428, - 428, - 437, - 449, - 453, - 453, - 455, - 455, - 501, - 536, - 546, - 640, - 689, - 728, - 776, - 743, - 711, - 738, - 645, - 652, - 637, - 626, - 628, - 599, - 609, - 593, - 553, - 545, - 501, - 481, - 449, - 430, - 425, - 423, - 416 - ], - "p50Micros": 432, - "p95Micros": 728, - "minMicros": 20, - "maxMicros": 776, - "wallMicros": 30564 - }, - "sqlMicros": [ - 57, - 57, - 59, - 47, - 49, - 47, - 48, - 47, - 47, - 47, - 48, - 47, - 47, - 48, - 47, - 47, - 47, - 47, - 47, - 48, - 47, - 47, - 47, - 47, - 47, - 47, - 47, - 45, - 48, - 47, - 58, - 59, - 52, - 48, - 47, - 47, - 90, - 86, - 61, - 140, - 66, - 74, - 93, - 57, - 57, - 88, - 61, - 84, - 57, - 62, - 58, - 52, - 64, - 50, - 48, - 50, - 48, - 45, - 45, - 45, - 45, - 46, - 45, - 46 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 9 - ], - "sqlMicros": [ - 59 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 9 - ], - "sqlMicros": [ - 62 - ], - "ormDecodeMicros": [ - 7 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365686852136, - "endMicros": 1365686852718, - "retainedRootRows": 100, - "rssBefore": 325025792, - "rssAfter": 325664768, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14579792, - "heapCapacity": 22396928, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503428", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 3504 - }, - { - "name": "_Map", - "instancesCurrent": 14, - "bytesCurrent": 896 - }, - { - "name": "_List", - "instancesCurrent": 40145, - "bytesCurrent": 3100672 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1130, - "bytesCurrent": 36160 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14600560, - "heapCapacity": 22396928, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503563", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 6704 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40252, - "bytesCurrent": 3108208 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 340, - "counts": { - "_List": 31, - "_Record": 303, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 300 - }, - { - "function": "_allocateData", - "samples": 21 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365687020967, - "endMicros": 1365687021895, - "retainedRootRows": 100, - "rssBefore": 335495168, - "rssAfter": 336412672, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14587024, - "heapCapacity": 22396928, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503594", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 109, - "bytesCurrent": 3504 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40148, - "bytesCurrent": 3101232 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1128, - "bytesCurrent": 36096 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14600688, - "heapCapacity": 22396928, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503735", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 209, - "bytesCurrent": 6704 - }, - { - "name": "_Map", - "instancesCurrent": 10, - "bytesCurrent": 640 - }, - { - "name": "_List", - "instancesCurrent": 40252, - "bytesCurrent": 3108208 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1232, - "bytesCurrent": 39424 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 1215, - "counts": { - "_List": 471, - "_SelectionPlan": 3, - "_GrowableList": 414, - "_Map": 18, - "_Record": 309 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 600 - }, - { - "function": "_allocateData", - "samples": 435 - }, - { - "function": "quote", - "samples": 36 - }, - { - "function": "_write", - "samples": 27 - }, - { - "function": "_List", - "samples": 17 - }, - { - "function": "writeSql", - "samples": 15 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_addPart", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_plan", - "samples": 6 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 938, - 751, - 742, - 822, - 753, - 740, - 762, - 755, - 742, - 774, - 785, - 773, - 741, - 748, - 728, - 783, - 747, - 734, - 744, - 724, - 729, - 820, - 764, - 743, - 734, - 817, - 726, - 730, - 725, - 721, - 723, - 730, - 721, - 784, - 732, - 719, - 726, - 865, - 727, - 811, - 741, - 724, - 751, - 735, - 720, - 732, - 777, - 746, - 763, - 735, - 729, - 733, - 725, - 726, - 729, - 727, - 731, - 738, - 729, - 764, - 749, - 734, - 725, - 737, - 728, - 756, - 735, - 731, - 746, - 731, - 730, - 732, - 725, - 735, - 726, - 803, - 723, - 729, - 728, - 727, - 726, - 720, - 724, - 720, - 771, - 728, - 723, - 733, - 756, - 741, - 740, - 728, - 738, - 818, - 727, - 727, - 733, - 732, - 728, - 817, - 721, - 724, - 722, - 721, - 717, - 735, - 720, - 714, - 736, - 718, - 715, - 801, - 816, - 768, - 729, - 732, - 721, - 720, - 720, - 736, - 778, - 733, - 725, - 729, - 730, - 751, - 727, - 730, - 734, - 733, - 719, - 713, - 717, - 725, - 716, - 728, - 719, - 788, - 728, - 721, - 717, - 745, - 812, - 735, - 755, - 719, - 709, - 714, - 716, - 719, - 719, - 711, - 716, - 712, - 723, - 752, - 715, - 766, - 743, - 723, - 713, - 709, - 713, - 720, - 717, - 803, - 717, - 710, - 715, - 719, - 709, - 715, - 722, - 712, - 738, - 721, - 719, - 720, - 721, - 720, - 720, - 719, - 719, - 780, - 725, - 723, - 717, - 716, - 734, - 726, - 730, - 731, - 719, - 720, - 728, - 730, - 719, - 729, - 720, - 722 - ], - "p50Micros": 728, - "p95Micros": 803, - "minMicros": 709, - "maxMicros": 938, - "wallMicros": 147559, - "operationsPerSecond": 1355.3900473708823 - }, - "orm": { - "samplesMicros": [ - 957, - 919, - 869, - 862, - 879, - 887, - 862, - 891, - 1031, - 931, - 915, - 889, - 923, - 909, - 852, - 856, - 896, - 879, - 859, - 866, - 825, - 827, - 852, - 841, - 839, - 909, - 894, - 845, - 831, - 815, - 893, - 824, - 822, - 859, - 837, - 819, - 869, - 908, - 815, - 814, - 841, - 833, - 818, - 900, - 824, - 822, - 844, - 826, - 984, - 861, - 826, - 819, - 847, - 823, - 830, - 842, - 865, - 965, - 837, - 867, - 837, - 808, - 815, - 818, - 837, - 837, - 895, - 824, - 824, - 810, - 846, - 823, - 817, - 821, - 817, - 813, - 822, - 808, - 808, - 810, - 804, - 812, - 805, - 808, - 906, - 822, - 819, - 814, - 814, - 826, - 807, - 814, - 826, - 804, - 849, - 835, - 807, - 804, - 801, - 896, - 813, - 798, - 862, - 815, - 807, - 799, - 798, - 797, - 801, - 802, - 821, - 993, - 1079, - 1087, - 818, - 803, - 814, - 799, - 800, - 795, - 835, - 820, - 823, - 803, - 802, - 873, - 811, - 806, - 845, - 892, - 803, - 796, - 797, - 796, - 798, - 796, - 790, - 833, - 856, - 792, - 806, - 813, - 819, - 810, - 811, - 803, - 803, - 860, - 798, - 788, - 787, - 791, - 793, - 791, - 788, - 792, - 862, - 842, - 814, - 819, - 789, - 794, - 792, - 798, - 785, - 795, - 795, - 788, - 782, - 784, - 790, - 792, - 791, - 783, - 885, - 817, - 799, - 798, - 796, - 791, - 798, - 796, - 798, - 795, - 800, - 827, - 795, - 815, - 799, - 799, - 796, - 800, - 878, - 801, - 791, - 792, - 799, - 798, - 794, - 800 - ], - "p50Micros": 817, - "p95Micros": 915, - "minMicros": 782, - "maxMicros": 1087, - "wallMicros": 166380, - "operationsPerSecond": 1202.0675561966582 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 758, - 1481, - 2212, - 2936, - 3780, - 4510, - 5250, - 6008, - 5984, - 5975, - 5987, - 5987, - 5856, - 5847, - 5831, - 5787, - 5810, - 5817, - 5787, - 5774, - 5780, - 5768, - 5796, - 5808, - 5775, - 5767, - 5774, - 5943, - 5950, - 5980, - 5951, - 5957, - 5974, - 5973, - 5961, - 5800, - 5793, - 5765, - 5760, - 5746, - 5730, - 5739, - 5749, - 5740, - 5729, - 5736, - 5750, - 5747, - 5745, - 5830, - 5829, - 5827, - 5833, - 5832, - 5851, - 5880, - 5896, - 5803, - 5801, - 5815, - 5815, - 5811, - 5785, - 5771, - 5755, - 5763, - 5759, - 5787, - 5807, - 5814, - 5819, - 5868, - 5881, - 5876, - 5886, - 5856, - 5864, - 5891, - 5914, - 5871, - 5893, - 5915, - 5932, - 5984, - 5982, - 5985, - 5968, - 5974, - 5963, - 5956, - 5973, - 5942, - 5932, - 5922, - 5923, - 5918, - 5914, - 5976, - 5952, - 5954, - 5961, - 5954, - 5950, - 5957, - 5961, - 5901, - 5913, - 5903, - 5907, - 5914, - 5932, - 5916, - 5927, - 5930, - 5916, - 5931, - 5931, - 5933, - 5920, - 5929, - 5922, - 5925, - 5935, - 5926, - 5933, - 5929, - 5960, - 5971, - 5981, - 5990, - 5992, - 6011, - 6012, - 6025, - 5996, - 6002, - 6020, - 6022, - 6022, - 6026, - 6023, - 6025, - 6029, - 6023, - 6004, - 6005, - 6260, - 6233, - 6197, - 6173, - 6142, - 6103, - 6083, - 6048, - 5757, - 5736, - 5735, - 5717, - 5711, - 5713, - 5698, - 5691, - 5696, - 5705, - 5712, - 5713, - 5726, - 5730, - 5733, - 5740, - 5736, - 5726, - 5729, - 5733, - 5721, - 5715, - 5716, - 5709, - 5715, - 5714, - 5715, - 5719, - 5728, - 5740, - 5756, - 5771, - 5764, - 5770, - 5765, - 5762, - 5755, - 5738, - 5713, - 5718, - 5737, - 5734, - 5729, - 5735, - 5727, - 5731 - ], - "p50Micros": 5847, - "p95Micros": 6025, - "minMicros": 758, - "maxMicros": 6260, - "wallMicros": 146643, - "operationsPerSecond": 1363.856440471076 - }, - "orm": { - "samplesMicros": [ - 905, - 1739, - 2583, - 3444, - 4348, - 5145, - 5937, - 6731, - 6629, - 6573, - 6519, - 6446, - 6347, - 6340, - 6332, - 6358, - 6353, - 6369, - 6386, - 6387, - 6369, - 6370, - 6429, - 6436, - 6487, - 6701, - 6746, - 6791, - 6813, - 6826, - 6789, - 6766, - 6732, - 6529, - 6478, - 6450, - 6446, - 6499, - 6497, - 6489, - 6487, - 6471, - 6465, - 6454, - 6452, - 6389, - 6387, - 6391, - 6383, - 6390, - 6394, - 6396, - 6391, - 6398, - 6400, - 6399, - 6399, - 6403, - 6404, - 6443, - 6472, - 6477, - 6469, - 6483, - 6508, - 6500, - 6501, - 6461, - 6453, - 6470, - 6492, - 6488, - 6490, - 6534, - 6555, - 6583, - 6582, - 6607, - 6586, - 6573, - 6689, - 6747, - 6773, - 6939, - 6920, - 6861, - 6860, - 6855, - 6706, - 6595, - 6545, - 6346, - 6332, - 6333, - 6329, - 6329, - 6341, - 6341, - 6334, - 6340, - 6345, - 6346, - 6354, - 6349, - 6352, - 6355, - 6359, - 6373, - 6379, - 6391, - 6387, - 6415, - 6404, - 6405, - 6400, - 6382, - 6375, - 6373, - 6375, - 6350, - 6357, - 6359, - 6357, - 6366, - 6367, - 6361, - 6360, - 6359, - 6355, - 6356, - 6378, - 6376, - 6385, - 6388, - 6392, - 6395, - 6441, - 6484, - 6508, - 6531, - 6533, - 6700, - 6720, - 6749, - 6715, - 6671, - 6633, - 6605, - 6590, - 6410, - 6385, - 6347, - 6338, - 6337, - 6328, - 6340, - 6339, - 6340, - 6328, - 6367, - 6383, - 6382, - 6384, - 6368, - 6373, - 6378, - 6409, - 6377, - 6356, - 6351, - 6350, - 6348, - 6350, - 6348, - 6324, - 6326, - 6336, - 6337, - 6345, - 6352, - 6354, - 6374, - 6370, - 6368, - 6358, - 6367, - 6368, - 6391, - 6386, - 6373, - 6376, - 6373, - 6385, - 6383, - 6381, - 6378, - 6380, - 6381, - 6376, - 6468 - ], - "p50Micros": 6386, - "p95Micros": 6766, - "minMicros": 905, - "maxMicros": 6939, - "wallMicros": 161576, - "operationsPerSecond": 1237.807595187404 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 11, - 780, - 1532, - 2265, - 2999, - 3736, - 4472, - 5213, - 5170, - 5157, - 5157, - 5158, - 5200, - 5257, - 5286, - 5309, - 5340, - 5377, - 5405, - 5398, - 5375, - 5640, - 5815, - 5778, - 5722, - 5667, - 5618, - 5567, - 5095, - 5029, - 5013, - 5014, - 5028, - 5016, - 5012, - 5024, - 5022, - 5024, - 5024, - 5021, - 5026, - 5079, - 5123, - 5127, - 5132, - 5131, - 5135, - 5130, - 5083, - 5026, - 5016, - 5014, - 5010, - 5004, - 5006, - 5012, - 5017, - 5028, - 5032, - 5037, - 5033, - 5032, - 5029, - 5034 - ], - "p50Micros": 5033, - "p95Micros": 5667, - "minMicros": 11, - "maxMicros": 5815, - "wallMicros": 311022 - }, - "sqlMicros": [ - 51, - 644, - 47, - 642, - 44, - 634, - 45, - 635, - 45, - 639, - 44, - 638, - 45, - 641, - 44, - 641, - 44, - 640, - 44, - 635, - 44, - 637, - 42, - 674, - 58, - 674, - 41, - 666, - 41, - 661, - 42, - 665, - 41, - 668, - 41, - 663, - 41, - 671, - 40, - 663, - 51, - 917, - 82, - 660, - 50, - 649, - 48, - 635, - 47, - 635, - 47, - 648, - 47, - 639, - 41, - 645, - 46, - 645, - 47, - 637, - 46, - 639, - 47, - 648, - 47, - 636, - 47, - 636, - 47, - 651, - 47, - 641, - 46, - 639, - 46, - 640, - 48, - 642, - 50, - 638, - 48, - 682, - 54, - 686, - 48, - 644, - 48, - 643, - 47, - 638, - 50, - 644, - 48, - 639, - 47, - 643, - 47, - 636, - 47, - 636, - 46, - 642, - 47, - 636, - 47, - 638, - 47, - 640, - 47, - 648, - 47, - 643, - 47, - 647, - 47, - 646, - 46, - 639, - 47, - 636, - 47, - 638, - 47, - 646, - 47, - 649, - 48, - 643 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 8, - 826, - 1623, - 2414, - 3208, - 4001, - 4793, - 5589, - 5547, - 5549, - 5561, - 5561, - 5566, - 5570, - 5562, - 5570, - 5561, - 5551, - 5550, - 5544, - 5666, - 5725, - 5724, - 5725, - 5722, - 5714, - 5736, - 5603, - 5544, - 5543, - 5541, - 5539, - 5543, - 5530, - 5527, - 5530, - 5528, - 5524, - 5522, - 5525, - 5516, - 5513, - 5517, - 5512, - 5517, - 5516, - 5521, - 5513, - 5512, - 5511, - 5519, - 5521, - 5527, - 5513, - 5519, - 5522, - 5521, - 5519, - 5519, - 5531, - 5545, - 5546, - 5552, - 5556 - ], - "p50Micros": 5530, - "p95Micros": 5724, - "minMicros": 8, - "maxMicros": 5736, - "wallMicros": 333723 - }, - "sqlMicros": [ - 47, - 645, - 49, - 641, - 47, - 640, - 48, - 641, - 48, - 641, - 47, - 641, - 47, - 648, - 46, - 641, - 47, - 648, - 47, - 653, - 47, - 647, - 48, - 648, - 51, - 643, - 47, - 639, - 47, - 649, - 46, - 641, - 47, - 643, - 48, - 643, - 47, - 639, - 48, - 652, - 65, - 657, - 49, - 643, - 46, - 640, - 46, - 641, - 46, - 641, - 53, - 664, - 46, - 643, - 46, - 639, - 46, - 647, - 45, - 644, - 45, - 640, - 45, - 644, - 45, - 653, - 46, - 640, - 49, - 638, - 45, - 645, - 46, - 639, - 46, - 637, - 46, - 648, - 46, - 646, - 45, - 638, - 46, - 644, - 45, - 637, - 45, - 645, - 45, - 638, - 45, - 653, - 45, - 639, - 45, - 638, - 50, - 640, - 46, - 648, - 47, - 645, - 47, - 641, - 46, - 639, - 46, - 643, - 46, - 640, - 45, - 641, - 47, - 647, - 45, - 647, - 45, - 652, - 45, - 647, - 45, - 646, - 45, - 647, - 45, - 646, - 45, - 640 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38, ?39, ?40, ?41, ?42, ?43, ?44, ?45, ?46, ?47, ?48, ?49, ?50, ?51, ?52, ?53, ?54, ?55, ?56, ?57, ?58, ?59, ?60, ?61, ?62, ?63, ?64, ?65, ?66, ?67, ?68, ?69, ?70, ?71, ?72, ?73, ?74, ?75, ?76, ?77, ?78, ?79, ?80, ?81, ?82, ?83, ?84, ?85, ?86, ?87, ?88, ?89, ?90, ?91, ?92, ?93, ?94, ?95, ?96, ?97, ?98, ?99, ?100))) AS \"orm_partition\" WHERE \"c3\" > ?101 AND \"c3\" <= ?102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 6 - ], - "sqlMicros": [ - 52, - 655 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38, ?39, ?40, ?41, ?42, ?43, ?44, ?45, ?46, ?47, ?48, ?49, ?50, ?51, ?52, ?53, ?54, ?55, ?56, ?57, ?58, ?59, ?60, ?61, ?62, ?63, ?64, ?65, ?66, ?67, ?68, ?69, ?70, ?71, ?72, ?73, ?74, ?75, ?76, ?77, ?78, ?79, ?80, ?81, ?82, ?83, ?84, ?85, ?86, ?87, ?88, ?89, ?90, ?91, ?92, ?93, ?94, ?95, ?96, ?97, ?98, ?99, ?100))) AS \"orm_partition\" WHERE \"c3\" > ?101 AND \"c3\" <= ?102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 6 - ], - "sqlMicros": [ - 61, - 667 - ], - "ormDecodeMicros": [ - 45, - 6 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365687244495, - "endMicros": 1365687248315, - "retainedRootRows": 100, - "rssBefore": 344752128, - "rssAfter": 347602944, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14817008, - "heapCapacity": 27115520, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503817", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 408, - "bytesCurrent": 14672 - }, - { - "name": "_Map", - "instancesCurrent": 13, - "bytesCurrent": 832 - }, - { - "name": "_List", - "instancesCurrent": 40049, - "bytesCurrent": 3118368 - }, - { - "name": "_GrowableList", - "instancesCurrent": 926, - "bytesCurrent": 29632 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14941824, - "heapCapacity": 19775488, - "externalUsage": 80 - }, - "gcTimestamp": "1789454503972", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 808, - "bytesCurrent": 29072 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40348, - "bytesCurrent": 3137440 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1228, - "bytesCurrent": 39296 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 2505, - "counts": { - "_List": 681, - "_Record": 1506, - "_GrowableList": 315, - "_Map": 3 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 1200 - }, - { - "function": "_allocateData", - "samples": 642 - }, - { - "function": "_List.of", - "samples": 303 - }, - { - "function": "current", - "samples": 300 - }, - { - "function": "_init", - "samples": 20 - }, - { - "function": "_sendInternal", - "samples": 18 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 6 - }, - { - "function": "_sliceInternal", - "samples": 3 - }, - { - "function": "_slice", - "samples": 3 - }, - { - "function": "Map._fromLiteral", - "samples": 3 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365687428350, - "endMicros": 1365687433726, - "retainedRootRows": 100, - "rssBefore": 356466688, - "rssAfter": 362594304, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14816320, - "heapCapacity": 19775488, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504001", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 408, - "bytesCurrent": 14672 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40044, - "bytesCurrent": 3117792 - }, - { - "name": "_GrowableList", - "instancesCurrent": 924, - "bytesCurrent": 29568 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14943264, - "heapCapacity": 21872640, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504213", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 808, - "bytesCurrent": 29072 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40349, - "bytesCurrent": 3137472 - }, - { - "name": "_GrowableList", - "instancesCurrent": 1228, - "bytesCurrent": 39296 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 9843, - "counts": { - "_List": 4782, - "_SelectionPlan": 12, - "_GrowableList": 3462, - "_Map": 60, - "_Record": 1527 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 3321 - }, - { - "function": "", - "samples": 3300 - }, - { - "function": "parameter", - "samples": 636 - }, - { - "function": "_GrowableList._ofGrowableList", - "samples": 600 - }, - { - "function": "_List", - "samples": 346 - }, - { - "function": "_List.of", - "samples": 309 - }, - { - "function": "load", - "samples": 303 - }, - { - "function": "add", - "samples": 300 - }, - { - "function": "quote", - "samples": 180 - }, - { - "function": "_write", - "samples": 135 - }, - { - "function": "_addPart", - "samples": 96 - }, - { - "function": "writeSql", - "samples": 81 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 35, - 36, - 39, - 36, - 35, - 34, - 38, - 35, - 39, - 35, - 30, - 36, - 42, - 45, - 43, - 35, - 37, - 37, - 38, - 35, - 38, - 35, - 39, - 35, - 43, - 35, - 37, - 38, - 37, - 35, - 37, - 34, - 37, - 35, - 38, - 36, - 37, - 35, - 37, - 35, - 42, - 35, - 41, - 35, - 37, - 35, - 38, - 35, - 37, - 35, - 37, - 35, - 38, - 35, - 38, - 35, - 38, - 39, - 37, - 35, - 40, - 37, - 38, - 38, - 38, - 36, - 36, - 37, - 39, - 37, - 36, - 36, - 37, - 36, - 41, - 36, - 39, - 35, - 36, - 40, - 39, - 36, - 38, - 35, - 36, - 36, - 38, - 36, - 37, - 35, - 38, - 35, - 40, - 36, - 37, - 36, - 37, - 38, - 38, - 36, - 35, - 36, - 40, - 36, - 38, - 36, - 39, - 36, - 44, - 36, - 38, - 36, - 35, - 36, - 43, - 39, - 38, - 35, - 37, - 38, - 39, - 38, - 44, - 36, - 36, - 36, - 39, - 36, - 37, - 36, - 35, - 38, - 35, - 35, - 37, - 36, - 36, - 36, - 36, - 36, - 36, - 36, - 37, - 39, - 33, - 37, - 35, - 35, - 37, - 35, - 36, - 36, - 38, - 62, - 34, - 37, - 37, - 34, - 32, - 35, - 32, - 36, - 36, - 33, - 32, - 35, - 37, - 35, - 36, - 35, - 36, - 35, - 34, - 31, - 36, - 39, - 36, - 35, - 36, - 35, - 33, - 36, - 34, - 37, - 28, - 36, - 36, - 35, - 36, - 35, - 36, - 35, - 36, - 38, - 29, - 57, - 47, - 32, - 34, - 34 - ], - "p50Micros": 36, - "p95Micros": 42, - "minMicros": 28, - "maxMicros": 62, - "wallMicros": 7348, - "operationsPerSecond": 27218.290691344584 - }, - "orm": { - "samplesMicros": [ - 40, - 41, - 42, - 40, - 44, - 40, - 40, - 40, - 41, - 39, - 40, - 40, - 52, - 47, - 49, - 41, - 40, - 65, - 43, - 40, - 43, - 40, - 41, - 39, - 42, - 39, - 41, - 39, - 40, - 40, - 41, - 39, - 40, - 39, - 40, - 39, - 40, - 40, - 41, - 39, - 41, - 39, - 40, - 39, - 40, - 39, - 40, - 39, - 40, - 39, - 40, - 40, - 40, - 39, - 41, - 40, - 40, - 39, - 40, - 39, - 40, - 40, - 41, - 41, - 47, - 39, - 42, - 42, - 44, - 41, - 40, - 41, - 42, - 40, - 41, - 41, - 42, - 41, - 41, - 40, - 41, - 41, - 41, - 41, - 42, - 40, - 41, - 41, - 42, - 42, - 43, - 41, - 42, - 41, - 42, - 41, - 47, - 40, - 41, - 41, - 41, - 43, - 44, - 41, - 41, - 41, - 42, - 41, - 43, - 40, - 41, - 41, - 41, - 40, - 42, - 41, - 41, - 43, - 43, - 42, - 40, - 41, - 41, - 41, - 42, - 40, - 41, - 44, - 40, - 41, - 41, - 39, - 41, - 39, - 101, - 57, - 39, - 39, - 39, - 38, - 39, - 38, - 39, - 38, - 47, - 40, - 39, - 38, - 39, - 38, - 50, - 43, - 49, - 46, - 49, - 39, - 39, - 38, - 38, - 39, - 39, - 38, - 39, - 38, - 38, - 38, - 38, - 37, - 38, - 37, - 38, - 38, - 46, - 44, - 39, - 38, - 39, - 38, - 39, - 37, - 39, - 38, - 46, - 52, - 38, - 37, - 39, - 37, - 38, - 38, - 38, - 38, - 39, - 47, - 49, - 46, - 63, - 52, - 50, - 46 - ], - "p50Micros": 40, - "p95Micros": 49, - "minMicros": 37, - "maxMicros": 101, - "wallMicros": 8304, - "operationsPerSecond": 24084.778420038536 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 53, - 95, - 134, - 173, - 242, - 283, - 319, - 356, - 354, - 342, - 336, - 333, - 300, - 297, - 293, - 290, - 274, - 270, - 272, - 267, - 272, - 272, - 287, - 284, - 284, - 291, - 301, - 303, - 297, - 297, - 285, - 292, - 296, - 295, - 289, - 290, - 298, - 298, - 298, - 301, - 298, - 294, - 307, - 306, - 296, - 298, - 298, - 296, - 300, - 300, - 284, - 287, - 288, - 285, - 285, - 284, - 280, - 283, - 286, - 285, - 286, - 287, - 287, - 286, - 290, - 289, - 289, - 289, - 288, - 288, - 288, - 289, - 289, - 283, - 280, - 280, - 280, - 280, - 280, - 279, - 279, - 285, - 289, - 285, - 286, - 286, - 285, - 288, - 288, - 289, - 292, - 308, - 305, - 307, - 307, - 305, - 305, - 307, - 303, - 297, - 296, - 293, - 294, - 294, - 294, - 292, - 292, - 285, - 289, - 290, - 291, - 288, - 288, - 288, - 288, - 289, - 291, - 289, - 288, - 306, - 308, - 307, - 307, - 306, - 304, - 304, - 322, - 313, - 332, - 358, - 356, - 352, - 350, - 353, - 336, - 330, - 312, - 286, - 291, - 296, - 299, - 296, - 296, - 296, - 318, - 363, - 412, - 430, - 467, - 497, - 549, - 556, - 570, - 539, - 513, - 499, - 506, - 503, - 455, - 465, - 439, - 458, - 456, - 472, - 425, - 394, - 389, - 374, - 361, - 327, - 287, - 282, - 284, - 289, - 289, - 287, - 286, - 287, - 289, - 292, - 292, - 292, - 292, - 294, - 295, - 295, - 291, - 288, - 289, - 286, - 283, - 272, - 271, - 267, - 268, - 268, - 267, - 270, - 274, - 275 - ], - "p50Micros": 292, - "p95Micros": 467, - "minMicros": 53, - "maxMicros": 570, - "wallMicros": 7923, - "operationsPerSecond": 25242.963523917708 - }, - "orm": { - "samplesMicros": [ - 67, - 106, - 143, - 184, - 222, - 259, - 296, - 333, - 311, - 307, - 305, - 315, - 328, - 336, - 342, - 351, - 360, - 367, - 376, - 366, - 358, - 352, - 346, - 337, - 327, - 320, - 312, - 307, - 307, - 306, - 306, - 306, - 306, - 310, - 318, - 325, - 328, - 336, - 344, - 351, - 358, - 394, - 391, - 388, - 380, - 372, - 364, - 356, - 348, - 308, - 303, - 299, - 300, - 300, - 300, - 300, - 300, - 300, - 302, - 303, - 302, - 302, - 302, - 302, - 302, - 303, - 299, - 299, - 299, - 300, - 299, - 300, - 300, - 299, - 299, - 299, - 298, - 298, - 298, - 297, - 297, - 297, - 297, - 298, - 298, - 299, - 300, - 303, - 303, - 304, - 305, - 305, - 305, - 304, - 303, - 300, - 299, - 300, - 299, - 298, - 297, - 298, - 297, - 297, - 297, - 297, - 297, - 298, - 298, - 298, - 298, - 299, - 299, - 299, - 299, - 299, - 300, - 311, - 340, - 351, - 355, - 360, - 361, - 361, - 362, - 356, - 338, - 328, - 326, - 322, - 327, - 327, - 327, - 321, - 311, - 310, - 309, - 308, - 302, - 302, - 302, - 301, - 300, - 300, - 301, - 300, - 300, - 300, - 299, - 300, - 300, - 300, - 300, - 304, - 312, - 319, - 327, - 335, - 342, - 349, - 357, - 359, - 360, - 360, - 359, - 359, - 354, - 355, - 354, - 354, - 354, - 361, - 363, - 428, - 433, - 430, - 424, - 419, - 414, - 405, - 404, - 362, - 358, - 357, - 359, - 357, - 354, - 348, - 345, - 318, - 319, - 318, - 315, - 315, - 315, - 314, - 309, - 304, - 299, - 287 - ], - "p50Micros": 307, - "p95Micros": 388, - "minMicros": 67, - "maxMicros": 433, - "wallMicros": 8142, - "operationsPerSecond": 24563.989191844754 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 10, - 52, - 100, - 137, - 174, - 210, - 246, - 282, - 264, - 251, - 252, - 248, - 251, - 263, - 265, - 267, - 267, - 269, - 273, - 274, - 263, - 263, - 262, - 262, - 266, - 266, - 261, - 258, - 257, - 256, - 255, - 250, - 249, - 249, - 250, - 250, - 250, - 250, - 249, - 249, - 249, - 249, - 249, - 249, - 249, - 249, - 249, - 248, - 245, - 245, - 246, - 246, - 246, - 246, - 246, - 249, - 249, - 249, - 248, - 249, - 249, - 248, - 248, - 247 - ], - "p50Micros": 249, - "p95Micros": 269, - "minMicros": 10, - "maxMicros": 282, - "wallMicros": 15417 - }, - "sqlMicros": [ - 41, - 46, - 35, - 36, - 34, - 35, - 34, - 34, - 35, - 34, - 31, - 38, - 46, - 36, - 36, - 35, - 36, - 35, - 39, - 37, - 36, - 35, - 35, - 40, - 35, - 34, - 34, - 34, - 34, - 34, - 34, - 34, - 34, - 35, - 34, - 34, - 34, - 34, - 34, - 34, - 35, - 34, - 34, - 34, - 34, - 34, - 34, - 32, - 34, - 34, - 34, - 34, - 34, - 34, - 35, - 34, - 34, - 34, - 34, - 34, - 34, - 34, - 34, - 34 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 7, - 52, - 95, - 133, - 179, - 223, - 269, - 312, - 295, - 294, - 293, - 284, - 277, - 270, - 265, - 261, - 257, - 259, - 259, - 260, - 260, - 260, - 260, - 260, - 257, - 257, - 256, - 256, - 255, - 256, - 256, - 255, - 256, - 256, - 256, - 256, - 255, - 255, - 255, - 255, - 256, - 256, - 256, - 256, - 256, - 256, - 255, - 255, - 255, - 255, - 255, - 255, - 256, - 255, - 255, - 255, - 255, - 255, - 254, - 254, - 254, - 257, - 256, - 256 - ], - "p50Micros": 256, - "p95Micros": 293, - "minMicros": 7, - "maxMicros": 312, - "wallMicros": 15804 - }, - "sqlMicros": [ - 35, - 33, - 30, - 37, - 36, - 36, - 34, - 33, - 33, - 30, - 29, - 29, - 29, - 29, - 29, - 30, - 32, - 29, - 30, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 30, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 30, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 29, - 30, - 29, - 29, - 29, - 29, - 29, - 29, - 28, - 29, - 29, - 32, - 28, - 28, - 28 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 5 - ], - "sqlMicros": [ - 50 - ], - "ormDecodeMicros": [] - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= ?1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 4 - ], - "sqlMicros": [ - 45 - ], - "ormDecodeMicros": [ - 1 - ] - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365687676371, - "endMicros": 1365687676713, - "retainedRootRows": 10, - "rssBefore": 373866496, - "rssAfter": 373964800, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14725072, - "heapCapacity": 22921216, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504249", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 18, - "bytesCurrent": 592 - }, - { - "name": "_Map", - "instancesCurrent": 13, - "bytesCurrent": 832 - }, - { - "name": "_List", - "instancesCurrent": 40059, - "bytesCurrent": 3118288 - }, - { - "name": "_GrowableList", - "instancesCurrent": 926, - "bytesCurrent": 29632 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14725488, - "heapCapacity": 22921216, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504384", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 28, - "bytesCurrent": 912 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40067, - "bytesCurrent": 3118544 - }, - { - "name": "_GrowableList", - "instancesCurrent": 938, - "bytesCurrent": 30016 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 62, - "counts": { - "_List": 23, - "_Record": 33, - "_GrowableList": 6 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "", - "samples": 30 - }, - { - "function": "_allocateData", - "samples": 12 - }, - { - "function": "_sendInternal", - "samples": 9 - }, - { - "function": "_instanceOf", - "samples": 6 - }, - { - "function": "_request", - "samples": 3 - }, - { - "function": "_init", - "samples": 1 - }, - { - "function": "_worker", - "samples": 1 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365687841538, - "endMicros": 1365687842091, - "retainedRootRows": 10, - "rssBefore": 382042112, - "rssAfter": 382271488, - "processLifetimeMaxRss": 531660800 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14723760, - "heapCapacity": 22921216, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504414", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 18, - "bytesCurrent": 592 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40053, - "bytesCurrent": 3117680 - }, - { - "name": "_GrowableList", - "instancesCurrent": 924, - "bytesCurrent": 29568 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 14725648, - "heapCapacity": 22921216, - "externalUsage": 80 - }, - "gcTimestamp": "1789454504553", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 28, - "bytesCurrent": 912 - }, - { - "name": "_Map", - "instancesCurrent": 9, - "bytesCurrent": 576 - }, - { - "name": "_List", - "instancesCurrent": 40067, - "bytesCurrent": 3118544 - }, - { - "name": "_GrowableList", - "instancesCurrent": 938, - "bytesCurrent": 30016 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 434, - "counts": { - "_List": 218, - "_SelectionPlan": 3, - "_GrowableList": 153, - "_Map": 18, - "_Record": 42 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 159 - }, - { - "function": "", - "samples": 60 - }, - { - "function": "quote", - "samples": 42 - }, - { - "function": "_write", - "samples": 30 - }, - { - "function": "_addPart", - "samples": 24 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "_instanceOf", - "samples": 12 - }, - { - "function": "Map._fromLiteral", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_List", - "samples": 12 - }, - { - "function": "_init", - "samples": 10 - }, - { - "function": "_sendInternal", - "samples": 9 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - } - ] - }, - { - "scenario": "postgres_loopback", - "environment": { - "backend": "postgres_loopback", - "databaseVersion": "18.4 (Homebrew)", - "pid": 94381 - }, - "oneWayDelayMs": 0, - "echoRoundTripMicros": { - "samplesMicros": [ - 181, - 197, - 177, - 200, - 183, - 184, - 413, - 208, - 188, - 188 - ], - "p50Micros": 188, - "p95Micros": 413, - "minMicros": 177, - "maxMicros": 413, - "wallMicros": 2119 - }, - "selectOneMicros": { - "samplesMicros": [ - 1014, - 804, - 784, - 785, - 783, - 761, - 764, - 791, - 827, - 755 - ], - "p50Micros": 784, - "p95Micros": 1014, - "minMicros": 755, - "maxMicros": 1014, - "wallMicros": 8068, - "operationsPerSecond": 1239.4645513138323 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 1057, - 2142, - 1044, - 1108, - 1062, - 1227, - 1088, - 1035, - 1034, - 1136, - 1024, - 1016, - 995, - 1065, - 995, - 989, - 988, - 971, - 977, - 971, - 983, - 1006, - 1087, - 1149, - 1027, - 977, - 998, - 1068, - 999, - 1009, - 1013, - 963, - 1017, - 965, - 968, - 956, - 943, - 958, - 1015, - 953, - 943, - 1195, - 988, - 931, - 919, - 949, - 952, - 963, - 913, - 934, - 974, - 922, - 1005, - 953, - 940, - 1013, - 973, - 899, - 931, - 1000, - 941, - 985, - 942, - 966, - 979, - 943, - 921, - 926, - 955, - 949, - 983, - 944, - 958, - 1029, - 958, - 960, - 934, - 1008, - 915, - 970, - 913, - 943, - 985, - 964, - 935, - 945, - 905, - 932, - 1012, - 945, - 937, - 976, - 947, - 915, - 942, - 954, - 929, - 881, - 930, - 955, - 907, - 969, - 1024, - 926, - 926, - 972, - 966, - 899, - 888, - 942, - 899, - 900, - 848, - 938, - 1011, - 957, - 917, - 900, - 895, - 914, - 939, - 898, - 875, - 915, - 842, - 866, - 848, - 857, - 851, - 868, - 845, - 852, - 858, - 852, - 835, - 823, - 885, - 855, - 896, - 848, - 848, - 968, - 909, - 868, - 869, - 825, - 854, - 870, - 841, - 855, - 872, - 819, - 867, - 902, - 920, - 902, - 966, - 874, - 894, - 955, - 846, - 878, - 878, - 882, - 902, - 906, - 845, - 894, - 892, - 881, - 866, - 889, - 882, - 896, - 927, - 866, - 903, - 946, - 900, - 873, - 885, - 864, - 875, - 892, - 844, - 924, - 872, - 917, - 975, - 898, - 878, - 874, - 920, - 873, - 859, - 937, - 881, - 893, - 892, - 896 - ], - "p50Micros": 931, - "p95Micros": 1062, - "minMicros": 819, - "maxMicros": 2142, - "wallMicros": 188471, - "operationsPerSecond": 1061.171214669631 - }, - "orm": { - "samplesMicros": [ - 1165, - 1206, - 1178, - 1099, - 1119, - 1234, - 1077, - 1187, - 1066, - 1093, - 1126, - 1046, - 1056, - 1092, - 1048, - 1022, - 1103, - 1017, - 1058, - 1035, - 1093, - 1050, - 1114, - 1055, - 1057, - 1048, - 1031, - 1358, - 1014, - 1026, - 1095, - 1133, - 1051, - 1011, - 1066, - 1044, - 1022, - 1039, - 1029, - 1040, - 1016, - 1036, - 1212, - 1017, - 1070, - 1089, - 1009, - 1052, - 1089, - 1000, - 1002, - 1006, - 1000, - 1009, - 1003, - 1011, - 1005, - 996, - 1002, - 1004, - 983, - 1023, - 1011, - 1105, - 1016, - 1016, - 1084, - 994, - 994, - 984, - 1048, - 1012, - 1001, - 971, - 1060, - 992, - 1022, - 1009, - 994, - 997, - 991, - 1095, - 1078, - 995, - 1084, - 994, - 990, - 1030, - 1018, - 1028, - 997, - 1018, - 987, - 1012, - 998, - 1079, - 967, - 957, - 1163, - 1074, - 934, - 979, - 983, - 1056, - 944, - 931, - 933, - 956, - 953, - 963, - 951, - 964, - 976, - 1005, - 966, - 941, - 999, - 966, - 975, - 957, - 956, - 944, - 895, - 921, - 929, - 952, - 882, - 960, - 972, - 926, - 908, - 1137, - 895, - 900, - 953, - 898, - 904, - 884, - 885, - 931, - 896, - 947, - 906, - 968, - 894, - 940, - 892, - 891, - 875, - 947, - 938, - 918, - 992, - 939, - 950, - 966, - 946, - 926, - 936, - 941, - 930, - 909, - 936, - 996, - 973, - 957, - 1058, - 935, - 931, - 933, - 998, - 939, - 936, - 941, - 976, - 931, - 933, - 916, - 986, - 943, - 943, - 1044, - 940, - 955, - 1007, - 1016, - 937, - 949, - 1046, - 926, - 915, - 937, - 961, - 987, - 950, - 935, - 946, - 908, - 949, - 982 - ], - "p50Micros": 994, - "p95Micros": 1126, - "minMicros": 875, - "maxMicros": 1358, - "wallMicros": 199764, - "operationsPerSecond": 1001.1813940449731 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 4199, - 5857, - 5926, - 5973, - 6011, - 7878, - 7921, - 7963, - 3843, - 4005, - 4104, - 4205, - 4086, - 3901, - 3919, - 3928, - 3934, - 3798, - 3795, - 3805, - 3812, - 3761, - 3891, - 3771, - 3863, - 3830, - 3829, - 3827, - 3824, - 3753, - 3754, - 3757, - 3754, - 3732, - 3747, - 3747, - 3744, - 4013, - 4026, - 4044, - 4052, - 3955, - 3951, - 3954, - 3961, - 3877, - 3866, - 3848, - 3843, - 3797, - 3785, - 3785, - 3784, - 3737, - 3743, - 3740, - 3736, - 3807, - 3820, - 3823, - 3823, - 3892, - 3882, - 3890, - 3902, - 4000, - 4001, - 4002, - 4010, - 3864, - 3865, - 3847, - 3982, - 3856, - 3854, - 3855, - 3842, - 3901, - 3903, - 3783, - 3909, - 3878, - 3886, - 3894, - 3905, - 3854, - 3857, - 3856, - 3859, - 3797, - 3788, - 3725, - 3822, - 3988, - 3992, - 3993, - 3991, - 3928, - 3936, - 3934, - 3933, - 3888, - 3885, - 3884, - 3880, - 3873, - 3867, - 3869, - 3871, - 3633, - 3682, - 3709, - 3738, - 3458, - 3501, - 3505, - 3514, - 3471, - 3462, - 3442, - 3438, - 3558, - 3512, - 3506, - 3494, - 3519, - 3473, - 3462, - 3444, - 3620, - 3619, - 3620, - 3621, - 3789, - 3796, - 3801, - 3798, - 4037, - 4044, - 4043, - 4043, - 4002, - 4006, - 4061, - 4073, - 4002, - 3997, - 4003, - 4003, - 3995, - 3995, - 3933, - 3914, - 3699, - 3689, - 3685, - 3694, - 3783, - 3783, - 3796, - 3803, - 3829, - 3839, - 3837, - 3826, - 3735, - 3730, - 3726, - 3719, - 3773, - 3773, - 3772, - 3769, - 3680, - 3678, - 3671, - 3674, - 3744, - 3750, - 3717, - 3801, - 4029, - 4054, - 4064, - 4073, - 4170, - 4306, - 4248, - 4260, - 4285, - 4278, - 4274, - 4274, - 4058, - 4186, - 4099, - 4085, - 3441, - 3394, - 3377 - ], - "p50Micros": 3854, - "p95Micros": 4274, - "minMicros": 3377, - "maxMicros": 7963, - "wallMicros": 99938, - "operationsPerSecond": 2001.2407692769518 - }, - "orm": { - "samplesMicros": [ - 2083, - 2173, - 2270, - 2356, - 4486, - 4635, - 4722, - 4811, - 4496, - 4493, - 4469, - 4464, - 4177, - 4138, - 4122, - 4111, - 4003, - 4232, - 4151, - 4160, - 4118, - 4111, - 4127, - 4136, - 4581, - 4572, - 4566, - 4572, - 4521, - 4437, - 4429, - 4711, - 4800, - 4892, - 4901, - 4896, - 5070, - 4969, - 5136, - 5040, - 4527, - 4321, - 4306, - 4602, - 4447, - 4453, - 4462, - 4449, - 4275, - 4271, - 4262, - 4251, - 4193, - 4185, - 4191, - 4211, - 4193, - 4201, - 4288, - 4312, - 4426, - 4422, - 4410, - 4397, - 4426, - 4424, - 4339, - 4304, - 3944, - 3942, - 3948, - 3938, - 4193, - 4183, - 4183, - 4191, - 4197, - 4191, - 4174, - 4186, - 4026, - 3958, - 4133, - 4056, - 4122, - 4300, - 4133, - 4299, - 4387, - 4638, - 4543, - 4577, - 4731, - 4740, - 4745, - 4732, - 4298, - 4251, - 4229, - 4201, - 5550, - 5570, - 5588, - 5611, - 6897, - 6917, - 6923, - 6929, - 5375, - 5358, - 5364, - 5349, - 4092, - 4278, - 4192, - 4571, - 4103, - 4302, - 4193, - 4184, - 4062, - 4060, - 4046, - 4037, - 3972, - 3981, - 4008, - 4036, - 3970, - 3982, - 3990, - 3999, - 4197, - 4179, - 4135, - 4105, - 4207, - 4203, - 4198, - 4189, - 4141, - 4142, - 4141, - 4141, - 3964, - 3963, - 3992, - 4015, - 4019, - 4027, - 4042, - 4056, - 4164, - 4204, - 4184, - 4166, - 4122, - 4159, - 4224, - 4243, - 4338, - 4283, - 4190, - 4343, - 4047, - 4276, - 4055, - 4025, - 3923, - 3940, - 3943, - 3946, - 4122, - 4127, - 4137, - 4141, - 4039, - 4040, - 4097, - 4125, - 4135, - 4146, - 4152, - 4146, - 4091, - 4088, - 4043, - 4016, - 3965, - 3940, - 3931, - 3930, - 4040, - 4042, - 4145, - 4156, - 4104, - 4052, - 4033, - 4002 - ], - "p50Micros": 4186, - "p95Micros": 5358, - "minMicros": 2083, - "maxMicros": 6929, - "wallMicros": 108814, - "operationsPerSecond": 1837.9987869208007 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 78, - 107, - 109, - 111, - 2081, - 2154, - 2210, - 2267, - 1981, - 1988, - 1971, - 1972, - 2412, - 2400, - 2389, - 2394, - 2519, - 2512, - 2504, - 2505, - 1893, - 1895, - 1899, - 1895, - 2998, - 2993, - 2995, - 3015, - 2055, - 2045, - 2040, - 2042, - 3381, - 3372, - 3383, - 3390, - 2044, - 2078, - 2075, - 2082, - 2328, - 2296, - 2285, - 2269, - 1915, - 1879, - 1901, - 1913, - 1936, - 1920, - 1888, - 1867, - 1795, - 1789, - 1791, - 1791, - 1757, - 1759, - 1762, - 1778, - 2410, - 2406, - 2421, - 2419 - ], - "p50Micros": 2045, - "p95Micros": 3372, - "minMicros": 78, - "maxMicros": 3390, - "wallMicros": 134509 - }, - "sqlMicros": [ - 1968, - 2022, - 2090, - 2145, - 1919, - 1921, - 1927, - 1926, - 2356, - 2347, - 2348, - 2347, - 2403, - 2412, - 2587, - 2460, - 1838, - 1851, - 1860, - 1853, - 2939, - 2957, - 2905, - 3017, - 1985, - 2002, - 2002, - 1989, - 3316, - 3331, - 3335, - 3340, - 1896, - 2090, - 2031, - 2039, - 2244, - 2242, - 2239, - 2226, - 1856, - 1832, - 1849, - 1864, - 1886, - 1882, - 1849, - 1824, - 1751, - 1758, - 1761, - 1758, - 1701, - 1673, - 1680, - 1827, - 2366, - 2375, - 2375, - 2381, - 1797, - 1888, - 1746, - 1832 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 77, - 573, - 602, - 621, - 3152, - 3233, - 3293, - 3362, - 2555, - 2553, - 2580, - 2584, - 3600, - 4170, - 4336, - 4374, - 2746, - 2198, - 2041, - 2007, - 1740, - 1700, - 1693, - 1741, - 1875, - 1863, - 1832, - 1778, - 1964, - 1971, - 2023, - 2052, - 2169, - 2167, - 2135, - 2122, - 2051, - 2055, - 2040, - 2046, - 1966, - 1939, - 1942, - 1932, - 1993, - 1991, - 1999, - 1995, - 1839, - 1843, - 1886, - 1924, - 2085, - 2072, - 2069, - 1993, - 1850, - 1844, - 1844, - 1904, - 2024, - 2006, - 2001, - 1947 - ], - "p50Micros": 1999, - "p95Micros": 3600, - "minMicros": 77, - "maxMicros": 4374, - "wallMicros": 136562 - }, - "sqlMicros": [ - 2560, - 2628, - 2668, - 2723, - 2459, - 2493, - 2518, - 2523, - 3503, - 4086, - 4261, - 4301, - 2620, - 2116, - 1962, - 1931, - 1651, - 1624, - 1627, - 1669, - 1797, - 1795, - 1772, - 1708, - 1891, - 1910, - 1921, - 1978, - 2091, - 2093, - 2050, - 2041, - 1961, - 1988, - 1979, - 1977, - 1866, - 1875, - 1884, - 1846, - 1906, - 1930, - 1941, - 1935, - 1757, - 1773, - 1774, - 1858, - 1922, - 1875, - 2165, - 1924, - 1760, - 1784, - 1784, - 1836, - 1945, - 1955, - 1930, - 1881, - 2037, - 2007, - 1972, - 1937 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 27 - ], - "sqlMicros": [ - 962 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 25 - ], - "sqlMicros": [ - 902 - ], - "ormDecodeMicros": [ - 13 - ], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365689545938, - "endMicros": 1365689551123, - "retainedRootRows": 100, - "rssBefore": 308805632, - "rssAfter": 310575104, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16890032, - "heapCapacity": 35717120, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506116", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 4992 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 46760, - "bytesCurrent": 3590960 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16946688, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506313", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 9792 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46784, - "bytesCurrent": 3594384 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 3698, - "counts": { - "_List": 2393, - "_GrowableList": 993, - "_Record": 312 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1983 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "_List", - "samples": 114 - }, - { - "function": "current", - "samples": 48 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 23 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_worker", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - }, - { - "function": "bindParameters", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365689769922, - "endMicros": 1365689773875, - "retainedRootRows": 100, - "rssBefore": 329187328, - "rssAfter": 330825728, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16900640, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506343", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 4992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46783, - "bytesCurrent": 3593344 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16953136, - "heapCapacity": 26591232, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506547", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 9792 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46790, - "bytesCurrent": 3595360 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 4937, - "counts": { - "_List": 3161, - "_GrowableList": 1437, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 318 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 2748 - }, - { - "function": "", - "samples": 903 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "_List", - "samples": 136 - }, - { - "function": "current", - "samples": 48 - }, - { - "function": "quote", - "samples": 48 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "_write", - "samples": 33 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 21 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 792, - 1600, - 797, - 780, - 809, - 901, - 813, - 823, - 851, - 793, - 761, - 792, - 837, - 811, - 769, - 788, - 788, - 808, - 758, - 748, - 730, - 717, - 729, - 779, - 805, - 801, - 793, - 824, - 771, - 804, - 765, - 809, - 778, - 808, - 760, - 821, - 760, - 855, - 788, - 807, - 791, - 758, - 781, - 559, - 537, - 555, - 553, - 524, - 591, - 534, - 532, - 550, - 539, - 535, - 524, - 541, - 524, - 539, - 533, - 547, - 535, - 549, - 560, - 598, - 553, - 522, - 529, - 549, - 535, - 535, - 528, - 532, - 536, - 530, - 549, - 544, - 520, - 533, - 512, - 587, - 543, - 524, - 534, - 529, - 569, - 578, - 536, - 532, - 540, - 528, - 519, - 519, - 527, - 545, - 662, - 547, - 512, - 530, - 512, - 508, - 515, - 531, - 515, - 560, - 518, - 576, - 509, - 517, - 487, - 552, - 485, - 513, - 503, - 532, - 495, - 509, - 504, - 497, - 488, - 491, - 545, - 521, - 493, - 502, - 493, - 508, - 488, - 484, - 504, - 496, - 548, - 507, - 493, - 489, - 479, - 533, - 489, - 501, - 492, - 491, - 499, - 489, - 479, - 490, - 514, - 545, - 502, - 496, - 493, - 498, - 487, - 486, - 484, - 502, - 476, - 541, - 570, - 513, - 522, - 524, - 527, - 534, - 528, - 534, - 523, - 541, - 544, - 491, - 489, - 499, - 493, - 555, - 496, - 503, - 492, - 535, - 496, - 523, - 557, - 487, - 488, - 537, - 498, - 503, - 501, - 504, - 519, - 503, - 480, - 488, - 499, - 498, - 517, - 487, - 496, - 493, - 489, - 494, - 462, - 495 - ], - "p50Micros": 530, - "p95Micros": 809, - "minMicros": 462, - "maxMicros": 1600, - "wallMicros": 116461, - "operationsPerSecond": 1717.3130919363564 - }, - "orm": { - "samplesMicros": [ - 857, - 830, - 852, - 822, - 852, - 2221, - 858, - 879, - 844, - 838, - 809, - 822, - 834, - 826, - 843, - 835, - 809, - 885, - 899, - 849, - 751, - 741, - 902, - 836, - 825, - 813, - 809, - 830, - 807, - 772, - 815, - 813, - 867, - 820, - 807, - 821, - 823, - 807, - 793, - 823, - 798, - 810, - 739, - 660, - 625, - 570, - 562, - 575, - 575, - 583, - 567, - 557, - 554, - 608, - 554, - 568, - 551, - 595, - 602, - 578, - 561, - 567, - 562, - 559, - 551, - 570, - 555, - 554, - 585, - 547, - 557, - 564, - 541, - 566, - 542, - 551, - 588, - 560, - 570, - 643, - 559, - 620, - 570, - 566, - 573, - 572, - 552, - 558, - 575, - 648, - 564, - 562, - 584, - 561, - 579, - 563, - 541, - 568, - 545, - 622, - 547, - 545, - 526, - 598, - 582, - 551, - 529, - 535, - 574, - 564, - 519, - 530, - 520, - 537, - 596, - 549, - 701, - 533, - 515, - 507, - 521, - 523, - 514, - 556, - 519, - 554, - 519, - 517, - 522, - 520, - 523, - 529, - 525, - 530, - 511, - 509, - 530, - 524, - 532, - 539, - 551, - 524, - 552, - 537, - 534, - 533, - 537, - 513, - 532, - 502, - 568, - 531, - 516, - 530, - 516, - 534, - 550, - 543, - 516, - 551, - 542, - 611, - 553, - 551, - 541, - 558, - 552, - 513, - 516, - 528, - 507, - 514, - 503, - 524, - 514, - 512, - 561, - 578, - 546, - 533, - 505, - 518, - 520, - 508, - 524, - 510, - 528, - 552, - 516, - 512, - 525, - 520, - 524, - 536, - 540, - 523, - 516, - 583, - 516, - 523 - ], - "p50Micros": 555, - "p95Micros": 849, - "minMicros": 502, - "maxMicros": 2221, - "wallMicros": 123046, - "operationsPerSecond": 1625.4083838564438 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 1122, - 1154, - 1179, - 1203, - 2487, - 2635, - 2692, - 2747, - 2765, - 2735, - 2841, - 2853, - 2217, - 2861, - 2850, - 2839, - 2125, - 2707, - 2705, - 2701, - 2387, - 2402, - 2448, - 2501, - 2576, - 3093, - 3097, - 3232, - 2979, - 3221, - 3065, - 3221, - 2694, - 2827, - 2793, - 2740, - 2627, - 2784, - 2786, - 2802, - 2838, - 2426, - 2796, - 2840, - 2964, - 2461, - 2971, - 3018, - 3029, - 3101, - 2877, - 3008, - 2961, - 2721, - 2774, - 2736, - 2966, - 2166, - 2576, - 2579, - 2585, - 2464, - 2365, - 2466, - 2416, - 2424, - 2400, - 2400, - 2360, - 2365, - 2362, - 2358, - 2357, - 2362, - 2346, - 2409, - 2443, - 2400, - 2410, - 2416, - 2418, - 2489, - 2436, - 2373, - 2333, - 2322, - 2314, - 2322, - 2564, - 2325, - 2555, - 2589, - 2895, - 2664, - 3172, - 3206, - 2969, - 2817, - 2990, - 2957, - 2652, - 2541, - 2309, - 2502, - 2352, - 2479, - 3116, - 3115, - 3076, - 3154, - 3163, - 3122, - 3200, - 3164, - 2698, - 2734, - 2707, - 2782, - 2837, - 3023, - 2975, - 3057, - 2713, - 2854, - 2764, - 2770, - 2587, - 2571, - 2570, - 2565, - 2649, - 2650, - 2648, - 2645, - 2564, - 2573, - 2572, - 2570, - 2547, - 2536, - 2534, - 2536, - 2439, - 2450, - 2453, - 2467, - 2390, - 2350, - 2351, - 2472, - 2563, - 2630, - 2588, - 2688, - 2700, - 2810, - 2764, - 2766, - 2886, - 2707, - 2808, - 2894, - 2606, - 2502, - 2623, - 2615, - 2477, - 2428, - 2520, - 2473, - 2528, - 2622, - 2579, - 2586, - 2665, - 2694, - 2682, - 2796, - 2963, - 3063, - 3015, - 3010, - 3012, - 2981, - 2867, - 2976, - 2659, - 2661, - 2622, - 2696, - 2567, - 2672, - 2806, - 2739, - 2688, - 2879, - 2830, - 2895, - 2220, - 2381 - ], - "p50Micros": 2652, - "p95Micros": 3116, - "minMicros": 1122, - "maxMicros": 3232, - "wallMicros": 67302, - "operationsPerSecond": 2971.67989064218 - }, - "orm": { - "samplesMicros": [ - 1302, - 1356, - 1436, - 1525, - 2667, - 2730, - 2796, - 2872, - 2887, - 2885, - 2764, - 2900, - 2772, - 2775, - 2766, - 2751, - 2733, - 2731, - 2686, - 2789, - 2715, - 2871, - 2796, - 2796, - 2598, - 2538, - 2663, - 2611, - 2677, - 2663, - 2654, - 2646, - 2677, - 2677, - 2614, - 2730, - 2576, - 2582, - 2593, - 2607, - 2792, - 2745, - 3000, - 2909, - 2897, - 2896, - 2820, - 2944, - 2757, - 2771, - 2721, - 2823, - 2733, - 2735, - 2747, - 2743, - 2873, - 2875, - 2878, - 2867, - 2801, - 2732, - 2847, - 2777, - 2388, - 2567, - 2705, - 2621, - 2789, - 2750, - 2863, - 2818, - 2852, - 2862, - 2812, - 2933, - 2813, - 2801, - 2787, - 2778, - 2653, - 2678, - 2960, - 2948, - 2837, - 3030, - 3024, - 2973, - 2955, - 3224, - 3083, - 3229, - 2956, - 3162, - 3108, - 3131, - 3213, - 3015, - 2990, - 3203, - 2957, - 2914, - 2895, - 2892, - 2661, - 2673, - 2675, - 2677, - 2793, - 2702, - 2825, - 2750, - 2730, - 2733, - 2676, - 2781, - 2735, - 2874, - 2764, - 2864, - 2753, - 3042, - 2998, - 3016, - 2954, - 3022, - 3027, - 3031, - 3231, - 3102, - 3009, - 3140, - 2934, - 2856, - 2878, - 2878, - 2710, - 2705, - 2702, - 2690, - 2709, - 2723, - 2696, - 2701, - 2668, - 2668, - 2657, - 2663, - 2777, - 2899, - 2868, - 2990, - 2974, - 3045, - 3012, - 3129, - 3015, - 2740, - 2867, - 3002, - 2932, - 2858, - 2777, - 2895, - 2799, - 3002, - 2883, - 3047, - 3095, - 3325, - 3213, - 3372, - 3103, - 3046, - 3058, - 3234, - 2824, - 2802, - 2788, - 2791, - 2619, - 2752, - 2882, - 2730, - 2586, - 2576, - 2573, - 2565, - 2771, - 2708, - 2819, - 2775, - 2837, - 2775, - 2773, - 2915, - 2791, - 2874, - 2781, - 2747 - ], - "p50Micros": 2796, - "p95Micros": 3140, - "minMicros": 1302, - "maxMicros": 3372, - "wallMicros": 70992, - "operationsPerSecond": 2817.2188415596124 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 50, - 53, - 54, - 55, - 1200, - 1250, - 1291, - 1325, - 1229, - 1216, - 1214, - 1213, - 1296, - 1289, - 1289, - 1292, - 1330, - 1352, - 1371, - 1379, - 1400, - 1395, - 1350, - 1339, - 1444, - 1468, - 1495, - 1519, - 1448, - 1418, - 1387, - 1367, - 1392, - 1367, - 1365, - 1368, - 1179, - 1179, - 1176, - 1180, - 1482, - 1523, - 1533, - 1538, - 1463, - 1400, - 1399, - 1403, - 1420, - 1408, - 1429, - 1443, - 1468, - 1439, - 1415, - 1400, - 1404, - 1383, - 1376, - 1371, - 1175, - 1167, - 1163, - 1162 - ], - "p50Micros": 1368, - "p95Micros": 1519, - "minMicros": 50, - "maxMicros": 1538, - "wallMicros": 81348 - }, - "sqlMicros": [ - 1138, - 1189, - 1226, - 1262, - 1181, - 1185, - 1180, - 1180, - 1253, - 1259, - 1221, - 1297, - 1246, - 1257, - 1417, - 1342, - 1359, - 1337, - 1318, - 1307, - 1386, - 1352, - 1486, - 1506, - 1390, - 1386, - 1359, - 1334, - 1352, - 1337, - 1303, - 1372, - 1150, - 1150, - 1114, - 1184, - 1404, - 1464, - 1543, - 1504, - 1353, - 1325, - 1413, - 1370, - 1344, - 1324, - 1340, - 1486, - 1410, - 1344, - 1328, - 1487, - 1308, - 1312, - 1437, - 1340, - 1145, - 1145, - 1142, - 1136, - 1235, - 1222, - 1209, - 1195 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 55, - 74, - 82, - 89, - 1161, - 1227, - 1283, - 1337, - 1542, - 1608, - 1689, - 1750, - 1629, - 1531, - 1528, - 1419, - 1430, - 1434, - 1427, - 1412, - 1457, - 1431, - 1416, - 1413, - 1337, - 1333, - 1334, - 1346, - 1419, - 1404, - 1409, - 1417, - 1543, - 1570, - 1603, - 1644, - 1441, - 1416, - 1379, - 1339, - 1452, - 1445, - 1453, - 1460, - 1470, - 1439, - 1426, - 1415, - 1349, - 1340, - 1347, - 1353, - 1177, - 1197, - 1194, - 1200, - 1149, - 1147, - 1143, - 1148, - 1310, - 1295, - 1293, - 1291 - ], - "p50Micros": 1409, - "p95Micros": 1629, - "minMicros": 55, - "maxMicros": 1750, - "wallMicros": 83851 - }, - "sqlMicros": [ - 1077, - 1137, - 1189, - 1235, - 1406, - 1442, - 1624, - 1743, - 1471, - 1469, - 1284, - 1448, - 1352, - 1370, - 1303, - 1420, - 1378, - 1376, - 1312, - 1419, - 1267, - 1289, - 1290, - 1300, - 1355, - 1365, - 1365, - 1357, - 1455, - 1507, - 1524, - 1555, - 1362, - 1374, - 1333, - 1294, - 1292, - 1459, - 1357, - 1467, - 1379, - 1388, - 1324, - 1427, - 1275, - 1200, - 1307, - 1404, - 1110, - 1088, - 1207, - 1153, - 1081, - 1093, - 1100, - 1101, - 1253, - 1249, - 1253, - 1242, - 1332, - 1190, - 1271, - 1351 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 24 - ], - "sqlMicros": [ - 563 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 23 - ], - "sqlMicros": [ - 554 - ], - "ormDecodeMicros": [ - 9 - ], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365690039901, - "endMicros": 1365690042708, - "retainedRootRows": 100, - "rssBefore": 342867968, - "rssAfter": 343752704, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16901888, - "heapCapacity": 29212672, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506612", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 3392 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 46833, - "bytesCurrent": 3599920 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16909648, - "heapCapacity": 22921216, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506796", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 6592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46828, - "bytesCurrent": 3600352 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 2665, - "counts": { - "_List": 1372, - "_GrowableList": 987, - "_Record": 306 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1062 - }, - { - "function": "", - "samples": 603 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "_List", - "samples": 12 - }, - { - "function": "_GrowableList._withData", - "samples": 12 - }, - { - "function": "_List.filled", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - }, - { - "function": "bindParameters", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365690252488, - "endMicros": 1365690255608, - "retainedRootRows": 100, - "rssBefore": 355418112, - "rssAfter": 356597760, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16900576, - "heapCapacity": 22921216, - "externalUsage": 272 - }, - "gcTimestamp": "1789454506825", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 106, - "bytesCurrent": 3392 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46827, - "bytesCurrent": 3599312 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 16909648, - "heapCapacity": 25542656, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507016", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 206, - "bytesCurrent": 6592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46828, - "bytesCurrent": 3600352 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 3548, - "counts": { - "_List": 1808, - "_GrowableList": 1407, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 312 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 1491 - }, - { - "function": "", - "samples": 903 - }, - { - "function": "wait", - "samples": 303 - }, - { - "function": "DataRowMessage.parse", - "samples": 300 - }, - { - "function": "handleMessage", - "samples": 300 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "quote", - "samples": 36 - }, - { - "function": "_List", - "samples": 34 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "_write", - "samples": 27 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "writeSql", - "samples": 15 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 1830, - 1802, - 1865, - 1783, - 1825, - 1744, - 1760, - 1806, - 1757, - 1819, - 1756, - 1813, - 1747, - 1813, - 1749, - 1792, - 1806, - 1764, - 1917, - 1732, - 1778, - 1753, - 1893, - 1691, - 1737, - 1712, - 1726, - 1771, - 1690, - 1745, - 1686, - 1772, - 1686, - 1824, - 1701, - 1716, - 1744, - 1782, - 2198, - 1741, - 1751, - 1694, - 1757, - 1759, - 1732, - 1751, - 1685, - 1762, - 1693, - 1789, - 1721, - 1745, - 1696, - 1667, - 1745, - 1694, - 1744, - 1679, - 1716, - 1675, - 1736, - 1724, - 1772, - 1719, - 1698, - 1783, - 1687, - 1771, - 1725, - 1738, - 1647, - 1675, - 1780, - 1723, - 1740, - 1689, - 1743, - 1710, - 1771, - 1695, - 1694, - 1764, - 1716, - 1744, - 1656, - 1784, - 1698, - 1730, - 1701, - 1710, - 1749, - 1704, - 1769, - 1715, - 1740, - 1709, - 1748, - 1737, - 1680, - 1739, - 1674, - 1762, - 1663, - 1759, - 1671, - 1766, - 1840, - 1686, - 1729, - 1707, - 1732, - 1703, - 1729, - 1662, - 1767, - 1681, - 1709, - 1708, - 1689, - 1870, - 2311, - 2337, - 2219, - 2313, - 2293, - 2222, - 2203, - 2047, - 1916, - 1675, - 1745, - 2226, - 2251, - 2192, - 2191, - 2255, - 2202, - 2268, - 2187, - 2237, - 2215, - 2179, - 2181, - 2216, - 2226, - 2203, - 2252, - 2123, - 2231, - 2218, - 2241, - 2152, - 2188, - 2226, - 2188, - 2240, - 2207, - 2514, - 2247, - 2228, - 2219, - 2218, - 2295, - 2156, - 2242, - 2165, - 2286, - 2245, - 2273, - 2213, - 2192, - 2190, - 2226, - 2236, - 2232, - 2233, - 2141, - 2097, - 2040, - 2055, - 2117, - 2076, - 2089, - 2154, - 2156, - 2049, - 2121, - 2224, - 2212, - 2276, - 2187, - 2287, - 2156, - 2238, - 2148, - 2290, - 2201, - 2224, - 2165, - 2236 - ], - "p50Micros": 1778, - "p95Micros": 2273, - "minMicros": 1647, - "maxMicros": 2514, - "wallMicros": 384526, - "operationsPerSecond": 520.1208760916037 - }, - "orm": { - "samplesMicros": [ - 2137, - 2003, - 1906, - 2032, - 1933, - 2066, - 1965, - 1961, - 2183, - 1960, - 2025, - 1895, - 2016, - 1973, - 1943, - 1908, - 1855, - 1976, - 1961, - 1988, - 1884, - 1989, - 1975, - 1903, - 1884, - 1921, - 1974, - 1855, - 1888, - 1845, - 1902, - 1822, - 2058, - 1856, - 1869, - 1970, - 1831, - 1926, - 1923, - 1921, - 1858, - 1851, - 1862, - 1968, - 2290, - 1865, - 1876, - 1815, - 2050, - 1824, - 1903, - 1809, - 1822, - 1850, - 1868, - 1951, - 1844, - 1919, - 1861, - 1971, - 1893, - 1889, - 1822, - 1822, - 1854, - 1826, - 1921, - 1760, - 1903, - 1789, - 1865, - 1789, - 1829, - 1865, - 1809, - 1869, - 1836, - 1895, - 1824, - 1870, - 1903, - 1853, - 1854, - 1770, - 1900, - 1802, - 1885, - 1841, - 1901, - 1943, - 1782, - 1861, - 1848, - 1871, - 1825, - 1850, - 1822, - 1877, - 1838, - 1791, - 1857, - 1772, - 1861, - 1797, - 1879, - 1777, - 1809, - 1877, - 1794, - 1895, - 1785, - 1867, - 1806, - 1845, - 1769, - 1839, - 1837, - 1793, - 1852, - 1857, - 2461, - 2344, - 2363, - 2329, - 2295, - 2346, - 2242, - 2275, - 1820, - 1869, - 2193, - 2361, - 2343, - 2377, - 2254, - 2291, - 2348, - 2431, - 2351, - 2328, - 2298, - 2217, - 2309, - 2343, - 2302, - 2333, - 2319, - 2347, - 2333, - 2326, - 2280, - 2331, - 2319, - 2262, - 2358, - 2300, - 2384, - 2329, - 2429, - 2308, - 2237, - 2383, - 2302, - 2401, - 2338, - 2351, - 2503, - 2418, - 2327, - 2401, - 2355, - 2287, - 2336, - 2290, - 2439, - 2246, - 2187, - 2097, - 2126, - 2168, - 2204, - 2247, - 2213, - 2208, - 2221, - 2313, - 2342, - 2288, - 2371, - 2309, - 2378, - 2297, - 2387, - 2273, - 2370, - 2328, - 2347, - 2341, - 2337, - 2378 - ], - "p50Micros": 1960, - "p95Micros": 2383, - "minMicros": 1760, - "maxMicros": 2503, - "wallMicros": 410846, - "operationsPerSecond": 486.8004069651402 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 4589, - 4635, - 4697, - 4746, - 9633, - 9704, - 9759, - 9825, - 9825, - 9836, - 9845, - 9863, - 10778, - 10838, - 10783, - 10918, - 12298, - 12319, - 12606, - 12610, - 11167, - 11130, - 11132, - 11138, - 10496, - 10479, - 10468, - 10458, - 10484, - 10480, - 10485, - 10492, - 9752, - 9751, - 9750, - 9747, - 9782, - 9780, - 9770, - 9761, - 9705, - 9708, - 9699, - 9694, - 9548, - 9548, - 9547, - 9546, - 9371, - 9388, - 9399, - 9405, - 9701, - 9704, - 9703, - 9707, - 9918, - 9917, - 9924, - 9928, - 9907, - 9915, - 9918, - 9903, - 9704, - 9881, - 9788, - 9791, - 9884, - 9876, - 9872, - 9878, - 9979, - 9980, - 9977, - 9978, - 9736, - 9722, - 9720, - 9721, - 9378, - 9560, - 9484, - 9491, - 9594, - 9596, - 9594, - 9592, - 9714, - 9724, - 9720, - 9715, - 9462, - 9751, - 9759, - 9757, - 9924, - 9902, - 9904, - 9902, - 10013, - 9745, - 9744, - 9759, - 9636, - 9637, - 9629, - 9628, - 9527, - 9526, - 9531, - 9532, - 9669, - 9824, - 9730, - 9733, - 9827, - 9831, - 9839, - 9829, - 10454, - 10400, - 10393, - 10593, - 10335, - 10331, - 10314, - 10308, - 9543, - 9694, - 9610, - 9608, - 9592, - 9610, - 9619, - 9624, - 9483, - 9486, - 9422, - 9554, - 9639, - 9624, - 9618, - 9618, - 9376, - 9580, - 9521, - 9513, - 9504, - 9503, - 9500, - 9506, - 9222, - 9184, - 9182, - 9187, - 9219, - 9385, - 9300, - 9300, - 9627, - 9794, - 9709, - 9711, - 9720, - 9724, - 9731, - 9731, - 9765, - 9925, - 9836, - 9835, - 9943, - 9937, - 9941, - 9935, - 9991, - 10006, - 10016, - 10030, - 9949, - 9955, - 9937, - 9930, - 9655, - 9647, - 9643, - 9645, - 9514, - 9502, - 9498, - 9497, - 9845, - 10012, - 9925, - 9905, - 9821, - 9822, - 9826, - 9820 - ], - "p50Micros": 9745, - "p95Micros": 10783, - "minMicros": 4589, - "maxMicros": 12610, - "wallMicros": 246626, - "operationsPerSecond": 810.9445070673813 - }, - "orm": { - "samplesMicros": [ - 4978, - 5089, - 5193, - 5303, - 9955, - 10086, - 10195, - 10299, - 10440, - 10708, - 10593, - 10600, - 10622, - 10622, - 10632, - 10629, - 10385, - 10376, - 10383, - 10396, - 10356, - 10351, - 10340, - 10345, - 10074, - 10332, - 10179, - 10158, - 10244, - 10261, - 10261, - 10271, - 10488, - 10502, - 10501, - 10501, - 10784, - 10779, - 10670, - 10901, - 13410, - 13419, - 13436, - 13458, - 13417, - 13412, - 13400, - 13401, - 10761, - 10737, - 10731, - 10711, - 10548, - 10542, - 10537, - 10539, - 10479, - 10489, - 10486, - 10485, - 11042, - 11122, - 11487, - 11266, - 11600, - 11882, - 11658, - 11885, - 11177, - 10887, - 11272, - 11266, - 11167, - 11172, - 11052, - 11305, - 11143, - 11146, - 11144, - 11149, - 11159, - 11009, - 11003, - 11403, - 10986, - 11279, - 11124, - 11118, - 11021, - 11016, - 11024, - 11009, - 10649, - 11248, - 11286, - 11313, - 10922, - 10938, - 10925, - 10919, - 10785, - 10754, - 11270, - 10848, - 11063, - 11087, - 10994, - 11239, - 10848, - 11139, - 10866, - 11105, - 10745, - 10718, - 10572, - 10822, - 10717, - 11007, - 10869, - 10870, - 10952, - 10687, - 11044, - 11037, - 10688, - 10996, - 10744, - 11047, - 11126, - 11224, - 11263, - 11290, - 11811, - 12167, - 12006, - 11945, - 12220, - 11996, - 12261, - 12096, - 11374, - 11719, - 11384, - 11641, - 10972, - 10951, - 10808, - 11039, - 10580, - 10576, - 10566, - 10543, - 10443, - 10684, - 10556, - 10565, - 10401, - 10680, - 10535, - 10530, - 10363, - 10598, - 10488, - 10473, - 10429, - 10430, - 10431, - 10437, - 10292, - 10528, - 10407, - 10419, - 10411, - 10422, - 10417, - 10411, - 10324, - 10579, - 10478, - 10470, - 10455, - 10726, - 10485, - 10696, - 10306, - 11201, - 11127, - 11545, - 13496, - 13782, - 13643, - 13651, - 12649, - 13841, - 12783, - 12575, - 10330, - 10600, - 10427, - 10419 - ], - "p50Micros": 10779, - "p95Micros": 13410, - "minMicros": 4978, - "maxMicros": 13841, - "wallMicros": 275871, - "operationsPerSecond": 724.9765288848773 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 67, - 67, - 67, - 68, - 5095, - 5189, - 5275, - 5355, - 5047, - 5028, - 5014, - 5014, - 4919, - 4925, - 4936, - 4966, - 5238, - 5242, - 5249, - 5241, - 5403, - 5373, - 5369, - 5372, - 4761, - 4709, - 4680, - 4652, - 4692, - 4706, - 4709, - 4715, - 4791, - 4761, - 4742, - 4738, - 4575, - 4592, - 4619, - 4641, - 4709, - 4686, - 4675, - 4667, - 4935, - 4947, - 4949, - 4949, - 4770, - 4761, - 4767, - 4780, - 4801, - 4775, - 4774, - 4769, - 4621, - 4626, - 4624, - 4626, - 4846, - 4820, - 4829, - 4874 - ], - "p50Micros": 4774, - "p95Micros": 5369, - "minMicros": 67, - "maxMicros": 5403, - "wallMicros": 293182 - }, - "sqlMicros": [ - 1358, - 1410, - 1459, - 1498, - 3557, - 3627, - 3636, - 3759, - 1672, - 1620, - 1556, - 1499, - 3260, - 3332, - 3386, - 3432, - 1477, - 1435, - 1387, - 1335, - 3332, - 3405, - 3466, - 3535, - 1576, - 1518, - 1356, - 1492, - 3486, - 3635, - 3769, - 3759, - 1602, - 1545, - 1481, - 1415, - 3660, - 3742, - 3799, - 3861, - 1390, - 1343, - 1540, - 1292, - 3204, - 3239, - 3269, - 3302, - 1422, - 1397, - 1362, - 1327, - 3182, - 3250, - 3292, - 3329, - 1373, - 1344, - 1304, - 1269, - 3311, - 3417, - 3399, - 3417, - 1300, - 1286, - 1264, - 1233, - 3184, - 3242, - 3292, - 3344, - 1451, - 1402, - 1352, - 1301, - 3147, - 3253, - 3268, - 3308, - 1618, - 1598, - 1560, - 1519, - 3189, - 3320, - 3329, - 3367, - 1419, - 1374, - 1338, - 1302, - 3262, - 3324, - 3370, - 3416, - 1507, - 1464, - 1420, - 1375, - 3213, - 3260, - 3301, - 3340, - 1369, - 1334, - 1292, - 1250, - 3170, - 3232, - 3278, - 3318, - 1481, - 1442, - 1402, - 1360, - 3270, - 3373, - 3371, - 3452, - 1455, - 1437, - 1389, - 1316, - 3183, - 3282, - 3285, - 3310 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 54, - 74, - 80, - 85, - 4992, - 5115, - 5215, - 5319, - 5051, - 5055, - 5062, - 5059, - 5215, - 5189, - 5176, - 5187, - 5143, - 5148, - 5158, - 5156, - 5412, - 5369, - 5352, - 5352, - 5030, - 5040, - 5054, - 5060, - 5239, - 5217, - 5205, - 5209, - 5009, - 5040, - 5058, - 5069, - 5362, - 5331, - 5314, - 5294, - 5072, - 5069, - 5092, - 5109, - 5242, - 5226, - 5216, - 5310, - 5578, - 5654, - 5738, - 5729, - 5433, - 5352, - 5273, - 5212, - 5612, - 5644, - 5674, - 5708, - 5546, - 5509, - 5482, - 5457 - ], - "p50Micros": 5212, - "p95Micros": 5674, - "minMicros": 54, - "maxMicros": 5738, - "wallMicros": 316486 - }, - "sqlMicros": [ - 1270, - 1373, - 1417, - 1467, - 3488, - 3555, - 3608, - 3655, - 1541, - 1507, - 1469, - 1429, - 3245, - 3464, - 3452, - 3486, - 1552, - 1506, - 1467, - 1436, - 3473, - 3548, - 3575, - 3609, - 1522, - 1502, - 1461, - 1423, - 3334, - 3546, - 3551, - 3589, - 1711, - 1664, - 1621, - 1585, - 3519, - 3572, - 3600, - 3627, - 1511, - 1490, - 1455, - 1413, - 3270, - 3471, - 3460, - 3505, - 1461, - 1653, - 1495, - 1447, - 3502, - 3546, - 3580, - 3628, - 1493, - 1481, - 1451, - 1412, - 3261, - 3480, - 3458, - 3492, - 1603, - 1577, - 1528, - 1467, - 3551, - 3625, - 3662, - 3694, - 1499, - 1473, - 1447, - 1423, - 3362, - 3451, - 3490, - 3541, - 1576, - 1566, - 1529, - 1491, - 3372, - 3585, - 3531, - 3675, - 1582, - 1786, - 1522, - 1486, - 3627, - 3639, - 3965, - 4094, - 1875, - 1768, - 1638, - 1505, - 3292, - 3500, - 3490, - 3548, - 1705, - 1668, - 1607, - 1543, - 3677, - 3796, - 3895, - 3985, - 1737, - 1667, - 1583, - 1486, - 3605, - 3689, - 3750, - 3811, - 1658, - 1687, - 1636, - 1578, - 3271, - 3237, - 3266, - 3284 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 38 - ], - "sqlMicros": [ - 755, - 1434 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 23 - ], - "sqlMicros": [ - 844, - 1458 - ], - "ormDecodeMicros": [ - 72, - 10 - ], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365690579407, - "endMicros": 1365690587019, - "retainedRootRows": 100, - "rssBefore": 368705536, - "rssAfter": 372719616, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17183264, - "heapCapacity": 27115520, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507153", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 406, - "bytesCurrent": 14592 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 46988, - "bytesCurrent": 3633760 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17284688, - "heapCapacity": 28164096, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507421", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 806, - "bytesCurrent": 28992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46990, - "bytesCurrent": 3635088 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 11753, - "counts": { - "_List": 6158, - "_GrowableList": 4077, - "_Record": 1515, - "_Map": 3 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 4614 - }, - { - "function": "", - "samples": 2406 - }, - { - "function": "wait", - "samples": 1206 - }, - { - "function": "DataRowMessage.parse", - "samples": 1200 - }, - { - "function": "handleMessage", - "samples": 1200 - }, - { - "function": "current", - "samples": 378 - }, - { - "function": "bindParameters", - "samples": 309 - }, - { - "function": "_List._ofGrowableList", - "samples": 303 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 43 - }, - { - "function": "_GrowableList._withData", - "samples": 21 - }, - { - "function": "_List", - "samples": 18 - }, - { - "function": "_init", - "samples": 18 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365690877764, - "endMicros": 1365690888024, - "retainedRootRows": 100, - "rssBefore": 387825664, - "rssAfter": 394510336, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17187616, - "heapCapacity": 28164096, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507453", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 406, - "bytesCurrent": 14592 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46989, - "bytesCurrent": 3634048 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17288192, - "heapCapacity": 23969792, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507781", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 806, - "bytesCurrent": 28992 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 46995, - "bytesCurrent": 3635456 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 19172, - "counts": { - "_List": 10282, - "_GrowableList": 7282, - "_SelectionPlan": 12, - "_Map": 60, - "_Record": 1536 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 7391 - }, - { - "function": "", - "samples": 4506 - }, - { - "function": "wait", - "samples": 1206 - }, - { - "function": "DataRowMessage.parse", - "samples": 1200 - }, - { - "function": "handleMessage", - "samples": 1200 - }, - { - "function": "_GrowableList._ofEfficientLengthIterable", - "samples": 636 - }, - { - "function": "parameter", - "samples": 636 - }, - { - "function": "_List", - "samples": 366 - }, - { - "function": "_List._ofGrowableList", - "samples": 309 - }, - { - "function": "bindParameters", - "samples": 309 - }, - { - "function": "load", - "samples": 303 - }, - { - "function": "[]", - "samples": 300 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 588, - 603, - 626, - 517, - 400, - 386, - 388, - 382, - 368, - 376, - 372, - 377, - 369, - 359, - 372, - 364, - 379, - 362, - 408, - 369, - 357, - 360, - 354, - 352, - 349, - 347, - 360, - 353, - 347, - 349, - 368, - 367, - 357, - 336, - 346, - 346, - 351, - 365, - 344, - 331, - 355, - 328, - 347, - 346, - 364, - 335, - 348, - 355, - 358, - 347, - 352, - 343, - 351, - 330, - 367, - 352, - 345, - 359, - 348, - 345, - 355, - 361, - 355, - 345, - 354, - 353, - 361, - 366, - 350, - 361, - 369, - 361, - 371, - 339, - 349, - 353, - 363, - 355, - 353, - 367, - 358, - 351, - 354, - 397, - 369, - 363, - 364, - 348, - 364, - 364, - 362, - 363, - 364, - 364, - 358, - 359, - 382, - 362, - 363, - 350, - 348, - 356, - 355, - 345, - 354, - 357, - 354, - 360, - 362, - 348, - 347, - 342, - 355, - 368, - 361, - 349, - 378, - 391, - 364, - 373, - 369, - 362, - 353, - 362, - 356, - 385, - 453, - 385, - 359, - 336, - 352, - 324, - 342, - 336, - 333, - 325, - 329, - 341, - 333, - 365, - 381, - 337, - 346, - 355, - 340, - 326, - 332, - 330, - 333, - 330, - 337, - 336, - 329, - 320, - 358, - 321, - 324, - 333, - 328, - 324, - 332, - 323, - 318, - 335, - 330, - 336, - 331, - 318, - 328, - 329, - 321, - 328, - 332, - 325, - 328, - 331, - 334, - 324, - 340, - 321, - 346, - 358, - 369, - 333, - 347, - 355, - 328, - 340, - 352, - 333, - 338, - 340, - 340, - 327, - 326, - 331, - 326, - 345, - 345, - 331 - ], - "p50Micros": 352, - "p95Micros": 386, - "minMicros": 318, - "maxMicros": 626, - "wallMicros": 71140, - "operationsPerSecond": 2811.35788585887 - }, - "orm": { - "samplesMicros": [ - 655, - 650, - 646, - 635, - 419, - 398, - 380, - 369, - 379, - 402, - 392, - 398, - 393, - 456, - 406, - 388, - 377, - 380, - 377, - 385, - 374, - 370, - 350, - 368, - 376, - 385, - 368, - 388, - 367, - 358, - 367, - 378, - 362, - 367, - 370, - 379, - 359, - 370, - 359, - 380, - 370, - 379, - 382, - 377, - 362, - 386, - 367, - 377, - 378, - 377, - 359, - 363, - 371, - 366, - 361, - 363, - 358, - 379, - 365, - 371, - 362, - 357, - 360, - 371, - 363, - 378, - 361, - 375, - 354, - 371, - 353, - 393, - 350, - 378, - 366, - 388, - 366, - 380, - 360, - 358, - 404, - 396, - 413, - 377, - 361, - 380, - 378, - 384, - 366, - 371, - 373, - 354, - 365, - 388, - 373, - 374, - 400, - 380, - 374, - 369, - 368, - 380, - 360, - 374, - 366, - 375, - 353, - 365, - 371, - 375, - 360, - 372, - 361, - 385, - 369, - 384, - 379, - 377, - 367, - 376, - 364, - 373, - 360, - 373, - 371, - 374, - 436, - 409, - 370, - 361, - 426, - 356, - 356, - 361, - 348, - 347, - 344, - 352, - 335, - 340, - 357, - 353, - 351, - 355, - 351, - 352, - 339, - 344, - 338, - 338, - 336, - 370, - 351, - 354, - 341, - 345, - 349, - 352, - 338, - 340, - 329, - 350, - 342, - 354, - 338, - 363, - 343, - 347, - 352, - 335, - 333, - 335, - 344, - 361, - 343, - 360, - 334, - 351, - 344, - 344, - 344, - 353, - 375, - 350, - 351, - 340, - 357, - 350, - 347, - 357, - 356, - 346, - 346, - 346, - 348, - 347, - 344, - 356, - 358, - 350 - ], - "p50Micros": 365, - "p95Micros": 406, - "minMicros": 329, - "maxMicros": 655, - "wallMicros": 74265, - "operationsPerSecond": 2693.0586413519154 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 588, - 702, - 736, - 754, - 1177, - 1305, - 1338, - 1368, - 1270, - 1272, - 1276, - 1276, - 1267, - 1283, - 1308, - 1310, - 1277, - 1219, - 1278, - 1343, - 1150, - 1331, - 1159, - 1196, - 1141, - 1119, - 1060, - 1005, - 1167, - 1167, - 1089, - 1193, - 1221, - 1216, - 1218, - 1209, - 1133, - 1223, - 1228, - 1231, - 1128, - 1194, - 1189, - 1193, - 1100, - 1060, - 1055, - 1046, - 1081, - 1142, - 1146, - 1142, - 1170, - 1352, - 1319, - 1369, - 1308, - 1389, - 1457, - 1429, - 1360, - 1336, - 1344, - 1344, - 1151, - 1153, - 1159, - 1151, - 998, - 960, - 964, - 968, - 982, - 911, - 910, - 920, - 1007, - 1071, - 1083, - 1100, - 1183, - 1192, - 1190, - 1187, - 1110, - 1070, - 1049, - 1063, - 989, - 1090, - 1103, - 1108, - 1118, - 1139, - 1141, - 1130, - 1111, - 1026, - 1013, - 1066, - 983, - 1126, - 1189, - 1175, - 1161, - 1320, - 1313, - 1254, - 1227, - 1227, - 1177, - 1177, - 1058, - 1099, - 1188, - 1188, - 1125, - 1138, - 1250, - 1297, - 1287, - 1172, - 1125, - 1210, - 1245, - 1152, - 1136, - 1120, - 1172, - 1210, - 1252, - 1239, - 1229, - 1310, - 1199, - 1167, - 1158, - 1217, - 1232, - 1153, - 1127, - 1120, - 1343, - 1254, - 1366, - 1160, - 1284, - 1292, - 1296, - 1122, - 1097, - 1125, - 1120, - 1034, - 1069, - 1039, - 1094, - 1046, - 1205, - 1183, - 1192, - 1007, - 1185, - 1295, - 1113, - 1324, - 1117, - 1213, - 1288, - 1295, - 1085, - 1059, - 1138, - 1229, - 1109, - 1207, - 1143, - 1250, - 1215, - 1238, - 1212, - 1132, - 1239, - 1086, - 1185, - 1079, - 1102, - 1064, - 1199, - 1215, - 1106, - 1310, - 1376, - 1366, - 1241, - 1418, - 1396, - 1400, - 1176, - 1144 - ], - "p50Micros": 1175, - "p95Micros": 1366, - "minMicros": 588, - "maxMicros": 1457, - "wallMicros": 29813, - "operationsPerSecond": 6708.482876597457 - }, - "orm": { - "samplesMicros": [ - 600, - 662, - 737, - 795, - 1107, - 1197, - 1240, - 1323, - 1058, - 1041, - 1167, - 1083, - 1134, - 1165, - 1250, - 1220, - 1327, - 1252, - 1362, - 1383, - 1440, - 1391, - 1301, - 1336, - 1411, - 1254, - 1455, - 1265, - 1409, - 1404, - 1408, - 1365, - 1499, - 1442, - 1517, - 1480, - 1456, - 1442, - 1430, - 1416, - 1336, - 1332, - 1334, - 1334, - 1264, - 1278, - 1284, - 1299, - 1341, - 1376, - 1396, - 1393, - 1478, - 1459, - 1445, - 1431, - 1449, - 1422, - 1400, - 1406, - 1414, - 1413, - 1415, - 1415, - 1397, - 1404, - 1410, - 1407, - 1453, - 1451, - 1421, - 1488, - 1460, - 1436, - 1431, - 1425, - 1368, - 1374, - 1336, - 1405, - 1321, - 1331, - 1327, - 1330, - 1345, - 1348, - 1318, - 1382, - 1431, - 1434, - 1433, - 1431, - 1343, - 1325, - 1413, - 1381, - 1279, - 1378, - 1309, - 1373, - 1403, - 1370, - 1360, - 1359, - 1315, - 1307, - 1350, - 1367, - 1298, - 1267, - 1385, - 1386, - 1309, - 1396, - 1391, - 1390, - 1291, - 1251, - 1252, - 1302, - 1197, - 1201, - 1108, - 1102, - 1183, - 1214, - 1204, - 1158, - 1144, - 1142, - 1101, - 1290, - 1185, - 1212, - 1370, - 1264, - 1351, - 1402, - 1416, - 1308, - 1240, - 1232, - 1252, - 1240, - 1036, - 1175, - 1077, - 1073, - 1047, - 1137, - 1113, - 1133, - 1190, - 1195, - 1334, - 1336, - 1473, - 1389, - 1474, - 1466, - 1348, - 1473, - 1394, - 1565, - 1465, - 1464, - 1376, - 1378, - 1530, - 1504, - 1387, - 1217, - 1444, - 1499, - 1507, - 1499, - 1452, - 1475, - 1444, - 1526, - 1250, - 1184, - 1268, - 1318, - 1131, - 1348, - 1381, - 1560, - 1443, - 1540, - 1558, - 1604, - 1536, - 1475, - 1455, - 1357, - 1279, - 1282, - 1255, - 1095 - ], - "p50Micros": 1359, - "p95Micros": 1504, - "minMicros": 600, - "maxMicros": 1604, - "wallMicros": 33581, - "operationsPerSecond": 5955.748786516185 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 50, - 45, - 45, - 49, - 535, - 586, - 609, - 632, - 496, - 552, - 550, - 551, - 608, - 623, - 634, - 633, - 610, - 592, - 576, - 585, - 621, - 577, - 603, - 598, - 550, - 513, - 526, - 616, - 581, - 579, - 537, - 596, - 547, - 716, - 762, - 628, - 683, - 765, - 704, - 692, - 706, - 662, - 668, - 673, - 509, - 513, - 566, - 562, - 561, - 512, - 559, - 569, - 600, - 695, - 592, - 704, - 717, - 703, - 713, - 581, - 538, - 545, - 648, - 643 - ], - "p50Micros": 592, - "p95Micros": 716, - "minMicros": 45, - "maxMicros": 765, - "wallMicros": 36694 - }, - "sqlMicros": [ - 483, - 546, - 559, - 593, - 471, - 531, - 532, - 532, - 582, - 601, - 588, - 641, - 580, - 569, - 559, - 563, - 591, - 553, - 585, - 579, - 522, - 492, - 505, - 597, - 552, - 561, - 519, - 572, - 517, - 675, - 737, - 599, - 653, - 611, - 681, - 779, - 680, - 634, - 647, - 648, - 487, - 486, - 546, - 540, - 539, - 486, - 526, - 548, - 566, - 675, - 556, - 676, - 684, - 681, - 678, - 559, - 516, - 529, - 598, - 657, - 653, - 546, - 546, - 543 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 52, - 58, - 63, - 69, - 641, - 686, - 723, - 756, - 744, - 736, - 731, - 729, - 757, - 779, - 793, - 803, - 765, - 737, - 724, - 715, - 723, - 738, - 728, - 730, - 749, - 750, - 761, - 775, - 813, - 779, - 773, - 776, - 759, - 749, - 747, - 738, - 726, - 740, - 743, - 752, - 753, - 727, - 714, - 721, - 726, - 722, - 724, - 721, - 701, - 693, - 695, - 697, - 702, - 700, - 696, - 699, - 770, - 764, - 776, - 775, - 758, - 753, - 747, - 737 - ], - "p50Micros": 737, - "p95Micros": 779, - "minMicros": 52, - "maxMicros": 813, - "wallMicros": 44581 - }, - "sqlMicros": [ - 581, - 621, - 653, - 683, - 703, - 665, - 741, - 695, - 705, - 743, - 729, - 803, - 658, - 764, - 652, - 733, - 642, - 752, - 705, - 701, - 675, - 719, - 702, - 774, - 740, - 744, - 743, - 738, - 711, - 719, - 716, - 711, - 675, - 708, - 716, - 724, - 702, - 695, - 688, - 686, - 683, - 659, - 732, - 686, - 656, - 665, - 666, - 670, - 660, - 673, - 667, - 669, - 725, - 704, - 779, - 737, - 718, - 730, - 723, - 704, - 662, - 656, - 645, - 640 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 18 - ], - "sqlMicros": [ - 403 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 20 - ], - "sqlMicros": [ - 363 - ], - "ormDecodeMicros": [ - 5 - ], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - } - }, - "memory": { - "raw": { - "batch": { - "runs": 3, - "startMicros": 1365691261250, - "endMicros": 1365691262869, - "retainedRootRows": 10, - "rssBefore": 360300544, - "rssAfter": 360480768, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17100464, - "heapCapacity": 21872640, - "externalUsage": 272 - }, - "gcTimestamp": "1789454507835", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 16, - "bytesCurrent": 512 - }, - { - "name": "_Map", - "instancesCurrent": 25, - "bytesCurrent": 1600 - }, - { - "name": "_List", - "instancesCurrent": 47007, - "bytesCurrent": 3634736 - }, - { - "name": "_GrowableList", - "instancesCurrent": 922, - "bytesCurrent": 29504 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17099648, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789454508016", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 26, - "bytesCurrent": 832 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47002, - "bytesCurrent": 3634272 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 487, - "counts": { - "_List": 274, - "_GrowableList": 177, - "_Record": 36 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 234 - }, - { - "function": "", - "samples": 63 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "wait", - "samples": 33 - }, - { - "function": "DataRowMessage.parse", - "samples": 30 - }, - { - "function": "handleMessage", - "samples": 30 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - }, - { - "function": "_List", - "samples": 9 - }, - { - "function": "_GrowableList._withData", - "samples": 9 - }, - { - "function": "_GrowableList._ofEfficientLengthIterable", - "samples": 6 - }, - { - "function": "_List.filled", - "samples": 3 - }, - { - "function": "_prepare", - "samples": 3 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - }, - "orm": { - "batch": { - "runs": 3, - "startMicros": 1365691475427, - "endMicros": 1365691477652, - "retainedRootRows": 10, - "rssBefore": 368984064, - "rssAfter": 369278976, - "processLifetimeMaxRss": 523517952 - }, - "heapBefore": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17099152, - "heapCapacity": 22396928, - "externalUsage": 272 - }, - "gcTimestamp": "1789454508047", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 16, - "bytesCurrent": 512 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47001, - "bytesCurrent": 3634128 - }, - { - "name": "_GrowableList", - "instancesCurrent": 920, - "bytesCurrent": 29440 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "heapWithLastResult": { - "memoryUsage": { - "type": "MemoryUsage", - "heapUsage": 17110736, - "heapCapacity": 23445504, - "externalUsage": 272 - }, - "gcTimestamp": "1789454508225", - "classes": [ - { - "name": "_Record", - "instancesCurrent": 26, - "bytesCurrent": 832 - }, - { - "name": "_Map", - "instancesCurrent": 21, - "bytesCurrent": 1344 - }, - { - "name": "_List", - "instancesCurrent": 47005, - "bytesCurrent": 3636080 - }, - { - "name": "_GrowableList", - "instancesCurrent": 921, - "bytesCurrent": 29472 - }, - { - "name": "_SelectionPlan", - "instancesCurrent": 0, - "bytesCurrent": 0 - } - ] - }, - "allocationTraces": { - "tracedClasses": [ - "_Record", - "_Map", - "_List", - "_GrowableList", - "_SelectionPlan", - "_Combined" - ], - "sampleCount": 869, - "counts": { - "_List": 467, - "_GrowableList": 336, - "_SelectionPlan": 3, - "_Map": 18, - "_Record": 45 - }, - "truncatedStacks": 0, - "topAllocationFrames": [ - { - "function": "_allocateData", - "samples": 402 - }, - { - "function": "", - "samples": 93 - }, - { - "function": "current", - "samples": 42 - }, - { - "function": "quote", - "samples": 42 - }, - { - "function": "_GrowableList._withData", - "samples": 33 - }, - { - "function": "wait", - "samples": 33 - }, - { - "function": "_write", - "samples": 30 - }, - { - "function": "DataRowMessage.parse", - "samples": 30 - }, - { - "function": "handleMessage", - "samples": 30 - }, - { - "function": "_List", - "samples": 22 - }, - { - "function": "writeSql", - "samples": 21 - }, - { - "function": "[Native] /Users/seven/workspace/flutter/bin/cache/dart-sdk/bin/dartvm+0x2db208", - "samples": 18 - } - ], - "scope": "Selected allocation traces in the main query isolate; not all object allocations or allocated bytes. Timing is measured separately with tracing disabled." - } - } - } - } - ] - }, - { - "scenario": "postgres_delay_20ms", - "environment": { - "backend": "postgres_delay_20ms", - "databaseVersion": "18.4 (Homebrew)", - "pid": 94406 - }, - "oneWayDelayMs": 10, - "echoRoundTripMicros": { - "samplesMicros": [ - 26410, - 26461, - 25124, - 26778, - 25893, - 25760, - 25517, - 27314, - 27261, - 27420 - ], - "p50Micros": 26410, - "p95Micros": 27420, - "minMicros": 25124, - "maxMicros": 27420, - "wallMicros": 263938 - }, - "selectOneMicros": { - "samplesMicros": [ - 83154, - 82678, - 83276, - 81636, - 83551, - 82064, - 82557, - 82920, - 83182, - 80891 - ], - "p50Micros": 82678, - "p95Micros": 83551, - "minMicros": 80891, - "maxMicros": 83551, - "wallMicros": 825909, - "operationsPerSecond": 12.10787144830726 - }, - "cases": [ - { - "case": "full_rows", - "latency": { - "raw": { - "samplesMicros": [ - 83478, - 89359, - 88236, - 79029, - 83721, - 84105, - 84017, - 83472, - 82549, - 80607, - 79357, - 83298, - 80922, - 81369, - 85859, - 82201, - 86662, - 82706, - 85967, - 82724, - 84199, - 80070, - 80482, - 80974, - 81828, - 83020, - 82663, - 83353, - 82179, - 81666, - 82794, - 82077, - 84465, - 80310, - 80177, - 85562, - 85259, - 86908, - 82573, - 81066, - 81602, - 82970, - 83741, - 81330, - 85823, - 80699, - 80751, - 75864, - 78233, - 85627, - 83480, - 81705, - 83932, - 82931, - 81710, - 85656, - 81956, - 77207, - 78604, - 85666, - 82696, - 80567, - 83908, - 81793, - 84705, - 82263, - 82998, - 84736, - 80907, - 80163, - 77495, - 78452, - 78989, - 83645, - 81980, - 81417, - 85714, - 85110, - 84349, - 77773, - 85298, - 82617, - 80157, - 80939, - 81229, - 84324, - 85490, - 85085, - 82639, - 78802, - 81545, - 85597, - 84444, - 81963, - 79957, - 82914, - 84539, - 82206, - 84594, - 85889, - 83871, - 84781, - 84637, - 83730, - 83590, - 82767, - 79885, - 80145, - 80187, - 77302, - 80452, - 84818, - 79435, - 84755, - 83453, - 84735, - 83205, - 83543, - 80307, - 80132, - 80682, - 80910, - 80880, - 82820, - 85602, - 80306, - 82824, - 84849, - 85215, - 83243, - 80786, - 81007, - 81757, - 83239, - 83435, - 83768, - 83936, - 84768, - 82789, - 81038, - 81271, - 78454, - 80443, - 83249, - 83827, - 83428, - 83983, - 86236, - 83622, - 86031, - 80682, - 82347, - 84159, - 87045, - 81451, - 81393, - 82521, - 80864, - 84892, - 83681, - 85153, - 83349, - 81411, - 81439, - 82154, - 82338, - 81452, - 77913, - 83632, - 83622, - 82711, - 82748, - 81608, - 85910, - 81496, - 81431, - 82102, - 82869, - 80463, - 80134, - 80824, - 85812, - 83434, - 83784, - 83406, - 81415, - 81055, - 85269, - 82662, - 79933, - 83929, - 80925, - 79439, - 81565, - 82241, - 82454, - 83230, - 84352, - 82817, - 84314 - ], - "p50Micros": 82706, - "p95Micros": 85859, - "minMicros": 75864, - "maxMicros": 89359, - "wallMicros": 16516596, - "operationsPerSecond": 12.10903263602258 - }, - "orm": { - "samplesMicros": [ - 84576, - 84433, - 83967, - 82103, - 84096, - 84057, - 84498, - 84053, - 82988, - 84243, - 82899, - 84989, - 82611, - 81589, - 84790, - 87101, - 83075, - 83277, - 80635, - 84102, - 85706, - 82619, - 82422, - 81602, - 84760, - 85109, - 80589, - 84842, - 83358, - 81870, - 84287, - 82706, - 84942, - 83045, - 80784, - 85816, - 81394, - 85397, - 81480, - 84477, - 80834, - 81111, - 80852, - 83879, - 83493, - 83822, - 81511, - 83069, - 77830, - 86033, - 83912, - 86621, - 86311, - 85925, - 81302, - 85923, - 82009, - 84664, - 81421, - 82205, - 81736, - 80925, - 82415, - 87447, - 84209, - 81312, - 86752, - 81093, - 86588, - 83547, - 79458, - 79136, - 78187, - 82862, - 83714, - 85007, - 86301, - 83831, - 84143, - 82570, - 84382, - 85775, - 80459, - 82246, - 85117, - 85382, - 86316, - 82807, - 76114, - 78375, - 81422, - 81537, - 88650, - 87034, - 80176, - 81995, - 83045, - 85628, - 80700, - 86003, - 84631, - 84217, - 84246, - 81924, - 82605, - 84555, - 80428, - 81267, - 80810, - 78809, - 82642, - 84710, - 83397, - 84815, - 84683, - 84617, - 84298, - 81106, - 79936, - 81451, - 85204, - 84813, - 80248, - 83367, - 82557, - 83570, - 82910, - 83591, - 85181, - 85675, - 80092, - 79800, - 84951, - 84265, - 86332, - 84047, - 79678, - 85413, - 82928, - 80700, - 82343, - 80453, - 80556, - 81612, - 76902, - 85246, - 84403, - 85792, - 86555, - 83632, - 82142, - 86162, - 86055, - 81167, - 80170, - 80737, - 84043, - 85270, - 80638, - 82959, - 83094, - 84090, - 85445, - 83526, - 84552, - 80705, - 80228, - 79169, - 84940, - 84425, - 84659, - 84313, - 85581, - 87311, - 85210, - 84203, - 83825, - 84398, - 80673, - 80049, - 83998, - 80658, - 82246, - 84010, - 81656, - 81238, - 80267, - 80727, - 82540, - 84696, - 80022, - 79996, - 83672, - 85509, - 84816, - 83153, - 83648, - 85373, - 80638, - 84948 - ], - "p50Micros": 83526, - "p95Micros": 86316, - "minMicros": 76114, - "maxMicros": 88650, - "wallMicros": 16629923, - "operationsPerSecond": 12.026513893058915 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 80190, - 95701, - 95980, - 96228, - 163952, - 177218, - 177560, - 177929, - 167475, - 164370, - 164942, - 165558, - 164722, - 165260, - 166979, - 167422, - 161596, - 161870, - 161674, - 161480, - 160359, - 157984, - 159050, - 157328, - 159529, - 159456, - 159285, - 159454, - 158864, - 158794, - 158972, - 159156, - 158423, - 158567, - 159012, - 159316, - 161234, - 162379, - 162983, - 163185, - 161674, - 161956, - 162707, - 163559, - 160779, - 159628, - 159754, - 160820, - 161242, - 159957, - 159983, - 161044, - 164014, - 163439, - 163335, - 163284, - 164302, - 162974, - 165354, - 164926, - 159784, - 164363, - 165592, - 165681, - 156976, - 170308, - 168601, - 169242, - 157688, - 172298, - 171764, - 172646, - 176593, - 176859, - 177191, - 178541, - 181150, - 177542, - 178941, - 178823, - 169243, - 173587, - 172915, - 173629, - 171675, - 171483, - 171543, - 172725, - 176250, - 174061, - 174916, - 174543, - 177513, - 173482, - 174695, - 174659, - 176237, - 177853, - 177764, - 178427, - 175418, - 177145, - 178096, - 177677, - 178519, - 175249, - 176437, - 175796, - 177662, - 175283, - 176619, - 176494, - 170797, - 170476, - 169717, - 170143, - 166112, - 164520, - 164974, - 165470, - 163225, - 163045, - 163102, - 163136, - 163627, - 166204, - 166666, - 167231, - 168963, - 171728, - 172298, - 172438, - 179517, - 176900, - 176777, - 178482, - 183231, - 180003, - 179733, - 180542, - 178343, - 177697, - 179117, - 179030, - 178505, - 176397, - 177445, - 178306, - 177516, - 179679, - 180442, - 181203, - 176221, - 180176, - 180205, - 181645, - 179752, - 180504, - 181699, - 181372, - 184265, - 182376, - 183728, - 183710, - 182336, - 179487, - 179352, - 179171, - 175795, - 177038, - 177072, - 178445, - 172350, - 170286, - 169824, - 170659, - 172238, - 172044, - 172138, - 172234, - 178588, - 176198, - 177599, - 177494, - 178098, - 173664, - 174833, - 176015, - 171173, - 173844, - 173479, - 174128, - 173525, - 176422, - 176255, - 175860, - 178504, - 182656, - 182554, - 183546 - ], - "p50Micros": 172350, - "p95Micros": 181645, - "minMicros": 80190, - "maxMicros": 184265, - "wallMicros": 4297032, - "operationsPerSecond": 46.543753921311264 - }, - "orm": { - "samplesMicros": [ - 88989, - 90011, - 90350, - 90933, - 172286, - 172806, - 172941, - 173418, - 164165, - 164808, - 165749, - 163880, - 163365, - 163943, - 163653, - 163720, - 169470, - 170222, - 170837, - 171365, - 172628, - 173210, - 174506, - 174330, - 171671, - 174042, - 173528, - 172495, - 178098, - 177402, - 177534, - 179343, - 178916, - 178321, - 179206, - 180161, - 173447, - 174769, - 174265, - 176922, - 172450, - 172631, - 175156, - 174848, - 176119, - 176189, - 176184, - 175914, - 177128, - 176752, - 176637, - 178155, - 172606, - 173727, - 175933, - 175600, - 174816, - 175044, - 175529, - 177890, - 176126, - 177356, - 178786, - 180075, - 177863, - 178010, - 177001, - 178012, - 175455, - 176745, - 178645, - 178278, - 177298, - 178944, - 177781, - 178610, - 179806, - 180115, - 180292, - 182518, - 174480, - 176020, - 175946, - 177771, - 172704, - 174810, - 173354, - 174866, - 172336, - 172887, - 174761, - 173646, - 175845, - 174644, - 175236, - 175926, - 172686, - 172021, - 173948, - 171831, - 162803, - 163774, - 163525, - 163426, - 163753, - 163907, - 164343, - 164181, - 167760, - 168771, - 170053, - 170231, - 168525, - 168841, - 169236, - 170005, - 170914, - 172565, - 174235, - 172018, - 180298, - 179335, - 180853, - 180694, - 180690, - 180795, - 180671, - 181600, - 177078, - 178730, - 180002, - 179246, - 173140, - 173007, - 177355, - 175689, - 175933, - 175620, - 175717, - 177353, - 182118, - 185595, - 186944, - 183517, - 181444, - 183036, - 182281, - 182238, - 172925, - 174143, - 176156, - 175931, - 177391, - 176880, - 179423, - 178345, - 175671, - 176119, - 178725, - 177155, - 173137, - 174543, - 173258, - 174029, - 176716, - 178673, - 179891, - 177768, - 173866, - 174279, - 174175, - 175110, - 173333, - 174176, - 174343, - 175506, - 179757, - 178711, - 178756, - 180827, - 172607, - 173809, - 175549, - 175347, - 171679, - 172753, - 174181, - 173643, - 169342, - 168481, - 170339, - 169182, - 161536, - 161396, - 162061, - 161734, - 161051, - 160918, - 160831, - 160769 - ], - "p50Micros": 174810, - "p95Micros": 180853, - "minMicros": 88989, - "maxMicros": 186944, - "wallMicros": 4354001, - "operationsPerSecond": 45.93476207286126 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 96, - 108, - 111, - 114, - 80763, - 80870, - 80957, - 81044, - 85287, - 85699, - 87315, - 87681, - 90961, - 90802, - 90387, - 89993, - 89601, - 89432, - 88768, - 88766, - 85536, - 85493, - 85495, - 85546, - 87810, - 87735, - 87699, - 87686, - 88487, - 88625, - 88822, - 89201, - 88677, - 88450, - 88297, - 88084, - 90929, - 90933, - 90935, - 90946, - 87989, - 87886, - 87844, - 87989, - 88955, - 88872, - 88873, - 88728, - 86496, - 86799, - 87040, - 87183, - 88626, - 88206, - 88095, - 87988, - 83892, - 83910, - 83858, - 83889, - 93138, - 93001, - 92876, - 92771 - ], - "p50Micros": 87989, - "p95Micros": 92771, - "minMicros": 96, - "maxMicros": 93138, - "wallMicros": 5269045 - }, - "sqlMicros": [ - 80595, - 80755, - 80809, - 80910, - 84855, - 85351, - 86890, - 87494, - 88169, - 88310, - 91501, - 92433, - 87383, - 89701, - 88534, - 89907, - 84783, - 84881, - 84899, - 86360, - 86682, - 87927, - 87872, - 87425, - 87343, - 87562, - 89184, - 89393, - 88304, - 87624, - 87272, - 88873, - 89819, - 91073, - 91045, - 90683, - 86847, - 88086, - 87956, - 87651, - 87449, - 88370, - 89403, - 89323, - 85684, - 86231, - 87439, - 86937, - 86523, - 87435, - 89096, - 88765, - 83123, - 83211, - 84421, - 83604, - 92817, - 92880, - 92714, - 92595, - 83811, - 84550, - 84461, - 85305 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 458, - 1222, - 1383, - 1471, - 91027, - 91654, - 92136, - 92603, - 87689, - 87522, - 87546, - 87487, - 87786, - 87832, - 87809, - 87795, - 92580, - 92143, - 91749, - 91450, - 81477, - 81273, - 81261, - 81202, - 81522, - 81476, - 81459, - 81451, - 82462, - 82508, - 82589, - 82829, - 85078, - 85927, - 86608, - 86889, - 89054, - 88748, - 88224, - 88113, - 87155, - 87091, - 87115, - 87418, - 88755, - 88561, - 88294, - 87825, - 87614, - 87714, - 88463, - 88659, - 87820, - 87635, - 86991, - 86877, - 87080, - 87098, - 87086, - 87342, - 89719, - 89546, - 89353, - 88997 - ], - "p50Micros": 87522, - "p95Micros": 92136, - "minMicros": 458, - "maxMicros": 92603, - "wallMicros": 5231700 - }, - "sqlMicros": [ - 89327, - 90420, - 90475, - 91199, - 87072, - 86642, - 87650, - 87179, - 85753, - 86905, - 88471, - 88457, - 92059, - 91003, - 91563, - 92148, - 80925, - 81040, - 81440, - 81096, - 81332, - 81184, - 81380, - 81587, - 82139, - 82505, - 82486, - 82699, - 84511, - 85089, - 86452, - 86562, - 86358, - 87988, - 89491, - 87701, - 85331, - 85608, - 87837, - 88136, - 86165, - 88824, - 88025, - 88924, - 86951, - 86969, - 87227, - 89043, - 86599, - 87875, - 86598, - 86558, - 85957, - 87211, - 86747, - 86995, - 88676, - 89749, - 89056, - 88713, - 85960, - 86401, - 86941, - 86056 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 82 - ], - "sqlMicros": [ - 80561 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\", \"t0\".\"nickname\", \"t0\".\"score\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 25399 - ], - "outputUtf8JsonBytes": 25399, - "acquireMicros": [ - 235 - ], - "sqlMicros": [ - 83606 - ], - "ormDecodeMicros": [ - 113 - ], - "postgresProtocolBytes": { - "sent": 237, - "received": 27784 - } - } - } - }, - { - "case": "two_columns", - "latency": { - "raw": { - "samplesMicros": [ - 83680, - 82091, - 83139, - 82812, - 82802, - 83459, - 83974, - 83152, - 82774, - 79143, - 80479, - 81205, - 80276, - 84078, - 80953, - 82778, - 78832, - 80930, - 80796, - 77911, - 82889, - 79923, - 79576, - 80030, - 82023, - 82652, - 82863, - 78834, - 83614, - 82750, - 81988, - 81994, - 79775, - 76545, - 79607, - 82874, - 83875, - 83743, - 80801, - 85143, - 85326, - 81637, - 82737, - 81440, - 81226, - 82245, - 79797, - 83137, - 82245, - 80255, - 80208, - 82213, - 82383, - 81083, - 82118, - 81412, - 83717, - 84079, - 80693, - 79698, - 80804, - 79024, - 80870, - 80179, - 85908, - 82256, - 83393, - 80752, - 85194, - 83282, - 79266, - 81263, - 80332, - 79847, - 79988, - 83298, - 81379, - 82914, - 84733, - 82604, - 80408, - 82353, - 84633, - 80678, - 82831, - 78788, - 80639, - 84701, - 82767, - 81308, - 82344, - 83874, - 82139, - 80570, - 93926, - 78012, - 83076, - 81878, - 82041, - 81372, - 83508, - 83765, - 84143, - 82384, - 83991, - 83762, - 81926, - 80214, - 82083, - 81767, - 81913, - 81720, - 78569, - 84523, - 83432, - 81618, - 79660, - 84451, - 85670, - 78572, - 80754, - 81605, - 81850, - 82631, - 81974, - 84876, - 82483, - 84801, - 82439, - 84937, - 82100, - 79563, - 80557, - 77806, - 82985, - 84444, - 81992, - 82660, - 83648, - 82889, - 81200, - 79475, - 80222, - 80291, - 79668, - 83199, - 79802, - 82705, - 79952, - 83240, - 83440, - 84556, - 80330, - 81037, - 82767, - 79374, - 79005, - 83114, - 83152, - 84400, - 83640, - 84064, - 82684, - 79904, - 82862, - 79741, - 83007, - 81884, - 79780, - 82777, - 84639, - 84419, - 85461, - 83635, - 80352, - 81865, - 81880, - 78648, - 79335, - 80135, - 78399, - 85168, - 83327, - 80093, - 84398, - 83610, - 83351, - 81275, - 83585, - 85267, - 84607, - 80984, - 80641, - 82333, - 80808, - 79103, - 82474, - 80874, - 79879, - 81658 - ], - "p50Micros": 82083, - "p95Micros": 84876, - "minMicros": 76545, - "maxMicros": 93926, - "wallMicros": 16398176, - "operationsPerSecond": 12.19647843760184 - }, - "orm": { - "samplesMicros": [ - 80691, - 80753, - 81817, - 83879, - 80846, - 84842, - 77862, - 80834, - 83604, - 78983, - 81979, - 75936, - 80691, - 80156, - 79858, - 81816, - 79452, - 79695, - 82084, - 84543, - 83350, - 79530, - 80773, - 80307, - 82976, - 83074, - 83549, - 83782, - 79900, - 85599, - 85272, - 82741, - 81941, - 83847, - 79014, - 81700, - 79807, - 81061, - 82471, - 84921, - 81467, - 85104, - 82766, - 83431, - 79479, - 85229, - 80073, - 81652, - 77818, - 81075, - 81560, - 81063, - 83589, - 83501, - 83094, - 82115, - 77749, - 83007, - 79621, - 80582, - 80926, - 84158, - 83010, - 83800, - 83257, - 83500, - 82159, - 82857, - 83260, - 79827, - 79673, - 80291, - 80703, - 81928, - 81406, - 83646, - 80922, - 85142, - 81435, - 82237, - 83851, - 81422, - 79133, - 78778, - 82454, - 82946, - 80680, - 81032, - 84108, - 84524, - 81770, - 82396, - 89701, - 82561, - 80756, - 79673, - 78870, - 83810, - 83855, - 82161, - 85592, - 83397, - 82446, - 82409, - 82965, - 80683, - 80686, - 79676, - 81630, - 80495, - 83128, - 80972, - 84368, - 82873, - 80475, - 78973, - 81436, - 82641, - 77996, - 79921, - 82449, - 81604, - 84097, - 82440, - 82228, - 84669, - 81086, - 81505, - 79421, - 78096, - 85210, - 80804, - 80779, - 81674, - 82313, - 83750, - 78405, - 81496, - 82575, - 80564, - 85737, - 81616, - 84027, - 81095, - 81484, - 80233, - 83181, - 80928, - 81124, - 82946, - 79687, - 81447, - 82046, - 84224, - 81205, - 89256, - 79742, - 79475, - 80709, - 80774, - 81109, - 85005, - 82149, - 84606, - 83433, - 83726, - 82781, - 83673, - 79932, - 80984, - 84228, - 83923, - 82253, - 84854, - 83751, - 79773, - 83134, - 81499, - 83630, - 84234, - 78809, - 80250, - 80878, - 79209, - 84226, - 81685, - 83917, - 80976, - 85875, - 80829, - 80840, - 76810, - 79393, - 80794, - 80997, - 81412, - 79574, - 81372, - 85024, - 80027 - ], - "p50Micros": 81652, - "p95Micros": 85104, - "minMicros": 75936, - "maxMicros": 89701, - "wallMicros": 16378966, - "operationsPerSecond": 12.210783025009027 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 82815, - 83069, - 83263, - 83493, - 164131, - 164490, - 164720, - 165091, - 168270, - 169214, - 169149, - 169034, - 171360, - 172210, - 172132, - 172907, - 168992, - 168559, - 168435, - 169118, - 169922, - 170134, - 170764, - 171298, - 171064, - 172141, - 172331, - 177410, - 176261, - 176717, - 176730, - 176965, - 171992, - 172478, - 173325, - 167621, - 161585, - 160044, - 160584, - 160922, - 160444, - 160657, - 160938, - 160865, - 164331, - 164632, - 164853, - 165154, - 168532, - 168816, - 168966, - 169274, - 170915, - 171483, - 172441, - 172307, - 172759, - 173803, - 173741, - 173751, - 171220, - 172169, - 171494, - 172282, - 169387, - 169213, - 170371, - 171034, - 167908, - 168486, - 168321, - 169424, - 168750, - 169557, - 168896, - 169465, - 171299, - 172051, - 172754, - 172079, - 171226, - 172148, - 171748, - 171786, - 170182, - 169504, - 170086, - 170750, - 167810, - 168559, - 168321, - 169221, - 169532, - 169586, - 169636, - 169641, - 169893, - 170153, - 170584, - 170681, - 173464, - 174318, - 173972, - 175387, - 177073, - 177142, - 176858, - 177433, - 170886, - 171574, - 171213, - 172582, - 165329, - 166882, - 166683, - 166619, - 168864, - 168412, - 169210, - 169236, - 170737, - 170763, - 171565, - 172187, - 164998, - 164625, - 165725, - 165096, - 160571, - 160411, - 160900, - 161232, - 159256, - 159154, - 159016, - 159162, - 160204, - 160266, - 160473, - 160389, - 167418, - 167691, - 168111, - 168319, - 167945, - 168467, - 168517, - 168846, - 168802, - 168469, - 168507, - 169353, - 173139, - 172800, - 173400, - 173125, - 172932, - 172675, - 173296, - 173237, - 168616, - 168501, - 168344, - 168183, - 167272, - 167972, - 168705, - 167608, - 169275, - 169258, - 169404, - 169937, - 168842, - 169660, - 169073, - 169549, - 170749, - 170472, - 171063, - 170960, - 171674, - 172340, - 173064, - 172378, - 171863, - 171809, - 172851, - 173516, - 172669, - 173425, - 172743, - 173283, - 175905, - 175959, - 176921, - 176165, - 171362, - 172089, - 172603, - 172158 - ], - "p50Micros": 169586, - "p95Micros": 176165, - "minMicros": 82815, - "maxMicros": 177433, - "wallMicros": 4237262, - "operationsPerSecond": 47.2002911313957 - }, - "orm": { - "samplesMicros": [ - 88276, - 88807, - 89256, - 90009, - 177404, - 177863, - 178436, - 178909, - 175116, - 175345, - 175375, - 175222, - 170247, - 172250, - 171541, - 172293, - 169369, - 171145, - 169892, - 171124, - 166396, - 167435, - 166978, - 166376, - 163252, - 163904, - 163040, - 162754, - 162421, - 162387, - 162430, - 162459, - 163157, - 163774, - 164089, - 164547, - 165505, - 166289, - 166916, - 167733, - 167656, - 167499, - 166976, - 167586, - 169863, - 172019, - 170748, - 172656, - 169977, - 170890, - 170742, - 172270, - 170398, - 171628, - 170093, - 171070, - 172915, - 173965, - 174810, - 176085, - 169502, - 170659, - 171681, - 170650, - 170947, - 171097, - 172642, - 171214, - 175617, - 174776, - 175948, - 175878, - 172965, - 174593, - 173600, - 174780, - 171314, - 171518, - 172678, - 173751, - 169968, - 169408, - 169307, - 170325, - 168214, - 169275, - 168912, - 170701, - 169144, - 169991, - 169883, - 171232, - 170514, - 169972, - 169826, - 170965, - 169348, - 170762, - 171789, - 171188, - 173361, - 174759, - 174065, - 175032, - 173578, - 175294, - 174288, - 175519, - 167553, - 168606, - 170012, - 169838, - 173630, - 172473, - 173471, - 173202, - 170399, - 171621, - 171042, - 170308, - 162421, - 161931, - 162338, - 162257, - 162632, - 162921, - 163113, - 163412, - 167155, - 167849, - 168278, - 168701, - 173321, - 173592, - 174171, - 174488, - 172348, - 173558, - 174673, - 176006, - 170113, - 171569, - 171415, - 172700, - 166528, - 168991, - 168401, - 169373, - 165695, - 167046, - 167066, - 166907, - 169132, - 170719, - 171835, - 170846, - 169927, - 171370, - 170706, - 170688, - 168437, - 168042, - 168921, - 168491, - 169099, - 170072, - 171313, - 170100, - 171713, - 173711, - 173118, - 173926, - 175740, - 175290, - 176169, - 175730, - 172044, - 173027, - 173843, - 174828, - 171877, - 173921, - 173827, - 173804, - 174415, - 175382, - 176844, - 176616, - 168587, - 169682, - 170894, - 170098, - 171082, - 170590, - 170605, - 171785, - 176398, - 175473, - 174892, - 176390 - ], - "p50Micros": 170762, - "p95Micros": 176006, - "minMicros": 88276, - "maxMicros": 178909, - "wallMicros": 4271162, - "operationsPerSecond": 46.82566477225635 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 291, - 278, - 283, - 291, - 83682, - 83990, - 84277, - 84701, - 87845, - 87811, - 87772, - 87736, - 82214, - 81913, - 81692, - 81468, - 80457, - 80422, - 80416, - 80417, - 81281, - 81286, - 81296, - 81318, - 84327, - 84590, - 84775, - 84973, - 83449, - 83377, - 83388, - 83595, - 87126, - 87006, - 87044, - 87130, - 84708, - 84642, - 84700, - 84562, - 84470, - 84460, - 84418, - 84425, - 86981, - 86975, - 87068, - 87151, - 82201, - 81874, - 81538, - 81274, - 80417, - 80775, - 81060, - 81330, - 87373, - 87136, - 87046, - 87018, - 86627, - 86538, - 86479, - 86479 - ], - "p50Micros": 84327, - "p95Micros": 87736, - "minMicros": 278, - "maxMicros": 87845, - "wallMicros": 5053642 - }, - "sqlMicros": [ - 83254, - 83602, - 83994, - 84231, - 87527, - 86871, - 87875, - 87851, - 81458, - 82132, - 81875, - 81368, - 80377, - 80340, - 80435, - 80376, - 81094, - 81298, - 81304, - 81269, - 84087, - 84394, - 84735, - 84839, - 83043, - 83157, - 83219, - 83246, - 85685, - 87156, - 86795, - 87507, - 84419, - 84480, - 84027, - 84710, - 83504, - 84254, - 83942, - 85139, - 86056, - 86120, - 87520, - 87462, - 82044, - 81392, - 81850, - 81173, - 79914, - 80437, - 80951, - 81378, - 85823, - 86577, - 87689, - 87472, - 85750, - 85791, - 86612, - 87183, - 82486, - 82944, - 82582, - 82473 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 371, - 597, - 679, - 743, - 84057, - 84528, - 84930, - 85319, - 84919, - 84935, - 84958, - 85185, - 86076, - 85887, - 85835, - 85754, - 86395, - 86470, - 86428, - 86399, - 87291, - 87514, - 87655, - 88112, - 88040, - 87706, - 87563, - 87426, - 89057, - 89055, - 89068, - 88962, - 87974, - 87918, - 87802, - 87799, - 82046, - 82083, - 82140, - 82199, - 86501, - 86107, - 85824, - 85559, - 81805, - 81691, - 81612, - 81566, - 80528, - 80475, - 80490, - 80484, - 82122, - 82126, - 82133, - 82161, - 86570, - 87207, - 88329, - 88901, - 85365, - 85080, - 84918, - 83871 - ], - "p50Micros": 85365, - "p95Micros": 88962, - "minMicros": 371, - "maxMicros": 89068, - "wallMicros": 5119300 - }, - "sqlMicros": [ - 82997, - 83808, - 84111, - 84748, - 83931, - 84999, - 84242, - 85287, - 84656, - 85572, - 84852, - 86976, - 85924, - 86166, - 85644, - 86540, - 86247, - 87539, - 87311, - 87607, - 86770, - 85964, - 88480, - 87602, - 88152, - 89165, - 88766, - 88566, - 86228, - 87202, - 87960, - 88788, - 80780, - 82163, - 81498, - 82606, - 86054, - 85867, - 85218, - 85758, - 81606, - 81612, - 81541, - 81498, - 80380, - 80263, - 80500, - 80511, - 81997, - 82045, - 82062, - 82092, - 85969, - 86729, - 87374, - 88971, - 82743, - 85338, - 85072, - 83561, - 86068, - 86629, - 87323, - 86289 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 119 - ], - "sqlMicros": [ - 83481 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 100 - ], - "driverRowsUtf8JsonBytes": [ - 3393 - ], - "outputUtf8JsonBytes": 3393, - "acquireMicros": [ - 323 - ], - "sqlMicros": [ - 82312 - ], - "ormDecodeMicros": [ - 70 - ], - "postgresProtocolBytes": { - "sent": 212, - "received": 4693 - } - } - } - }, - { - "case": "three_posts", - "latency": { - "raw": { - "samplesMicros": [ - 169502, - 164980, - 161815, - 170277, - 166749, - 162675, - 171241, - 161994, - 162520, - 169251, - 172015, - 170438, - 169014, - 163334, - 163181, - 165430, - 164552, - 167452, - 165628, - 160747, - 162382, - 164361, - 167971, - 171751, - 170940, - 158390, - 162605, - 167328, - 160894, - 168829, - 164410, - 162689, - 160585, - 170015, - 169984, - 166256, - 169542, - 159688, - 163294, - 168422, - 165637, - 166953, - 165869, - 161217, - 160834, - 165252, - 169260, - 166918, - 172188, - 160916, - 161453, - 165565, - 169793, - 168941, - 165119, - 161375, - 163782, - 167072, - 164626, - 167251, - 171817, - 158704, - 163786, - 168744, - 168617, - 170543, - 168572, - 160529, - 165102, - 168089, - 164581, - 166563, - 169012, - 161352, - 164804, - 169634, - 161958, - 170811, - 166502, - 159315, - 162727, - 166993, - 167008, - 163974, - 164403, - 158897, - 161976, - 167042, - 162427, - 165098, - 163777, - 160961, - 161493, - 170886, - 166127, - 172565, - 166926, - 160699, - 166244, - 166401, - 164443, - 166362, - 167109, - 160224, - 161080, - 166460, - 164748, - 165833, - 167502, - 159010, - 166117, - 172216, - 171039, - 169273, - 165972, - 161299, - 165743, - 165652, - 168236, - 169565, - 166759, - 158630, - 168570, - 167543, - 168145, - 167886, - 170039, - 161350, - 164923, - 169822, - 163703, - 165049, - 165853, - 160208, - 166774, - 165902, - 167143, - 168927, - 164995, - 160960, - 166141, - 167765, - 169235, - 166159, - 165220, - 159978, - 168812, - 167435, - 168757, - 166134, - 166178, - 160446, - 166600, - 170372, - 166951, - 171896, - 166784, - 161147, - 167852, - 164871, - 164514, - 193041, - 184837, - 160101, - 166498, - 169804, - 165587, - 166388, - 168458, - 160611, - 169432, - 168766, - 171019, - 168175, - 183621, - 160965, - 161256, - 162489, - 162921, - 165931, - 166605, - 161458, - 167530, - 168521, - 169086, - 168048, - 169116, - 158475, - 168564, - 170407, - 174996, - 175560, - 164515, - 161357, - 164037, - 170859, - 168293, - 164047, - 168099, - 162012 - ], - "p50Micros": 166178, - "p95Micros": 171817, - "minMicros": 158390, - "maxMicros": 193041, - "wallMicros": 33234607, - "operationsPerSecond": 6.017823529551591 - }, - "orm": { - "samplesMicros": [ - 168859, - 172392, - 170984, - 167469, - 169633, - 169574, - 163388, - 170080, - 169642, - 169367, - 169460, - 173804, - 170698, - 171526, - 168983, - 163641, - 171453, - 167499, - 162818, - 171769, - 163397, - 166496, - 168793, - 168850, - 170911, - 168972, - 167354, - 166659, - 168371, - 171643, - 172014, - 175136, - 173051, - 166069, - 170223, - 164904, - 166967, - 169964, - 171182, - 163577, - 166177, - 171643, - 168265, - 174213, - 165794, - 159742, - 163465, - 164451, - 169332, - 164351, - 171567, - 166915, - 172536, - 171027, - 169852, - 171591, - 166509, - 165469, - 167577, - 174138, - 168168, - 168506, - 168732, - 167775, - 169383, - 170808, - 167854, - 165500, - 168460, - 164438, - 169735, - 171624, - 169501, - 164846, - 166433, - 169203, - 171375, - 172209, - 170410, - 163799, - 169878, - 170376, - 169666, - 165990, - 164471, - 172351, - 168170, - 164146, - 170755, - 166973, - 165128, - 162801, - 173419, - 169971, - 166357, - 166329, - 172932, - 167003, - 168691, - 168292, - 161640, - 163786, - 169020, - 167582, - 165915, - 168123, - 167299, - 169098, - 172906, - 167543, - 171865, - 167146, - 170314, - 171933, - 170722, - 170429, - 170424, - 169880, - 163413, - 169328, - 168816, - 163609, - 175647, - 187248, - 168159, - 165188, - 169486, - 165345, - 167700, - 171643, - 168622, - 167870, - 168273, - 160138, - 174182, - 170110, - 165982, - 171162, - 172007, - 161528, - 170549, - 162702, - 163681, - 164794, - 173045, - 162760, - 162014, - 166238, - 168294, - 166211, - 163250, - 166560, - 165138, - 168974, - 165265, - 168613, - 172111, - 163504, - 165880, - 167936, - 171511, - 173783, - 165423, - 164825, - 170391, - 168577, - 162722, - 171045, - 167328, - 161039, - 166321, - 165504, - 170738, - 169259, - 164849, - 157344, - 164443, - 162474, - 167248, - 165480, - 162678, - 161635, - 166149, - 164738, - 166584, - 168079, - 172238, - 162436, - 169530, - 167862, - 169012, - 171519, - 169824, - 159702, - 166327, - 172104, - 166748, - 164930, - 171771, - 161377 - ], - "p50Micros": 168265, - "p95Micros": 173045, - "minMicros": 157344, - "maxMicros": 187248, - "wallMicros": 33598791, - "operationsPerSecond": 5.952595139509633 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 176769, - 176972, - 177083, - 177241, - 355520, - 355710, - 355921, - 356147, - 359468, - 360118, - 360784, - 360711, - 363254, - 363861, - 364352, - 364053, - 358701, - 357717, - 358476, - 359445, - 355483, - 356890, - 356851, - 356886, - 366394, - 367074, - 367994, - 367758, - 361033, - 361008, - 362008, - 363557, - 353363, - 354387, - 354510, - 354743, - 348043, - 347583, - 349092, - 347120, - 331735, - 331025, - 330738, - 331558, - 340569, - 341553, - 341789, - 342233, - 355794, - 356556, - 356651, - 357141, - 356650, - 356693, - 358516, - 357045, - 358871, - 358786, - 358920, - 360084, - 360191, - 361662, - 361659, - 363251, - 355970, - 355911, - 355979, - 355983, - 354139, - 356209, - 356155, - 356263, - 347898, - 347497, - 347055, - 346737, - 330375, - 331622, - 331127, - 330243, - 335373, - 335642, - 336317, - 336103, - 333900, - 334149, - 334360, - 334615, - 330830, - 331925, - 332921, - 332440, - 343957, - 344434, - 344406, - 344928, - 355624, - 356854, - 356239, - 358592, - 348983, - 348816, - 348246, - 349119, - 333079, - 331710, - 332855, - 332512, - 336244, - 337254, - 337052, - 338231, - 348084, - 348461, - 348826, - 349810, - 353408, - 352709, - 352542, - 354079, - 355733, - 354709, - 355447, - 356336, - 358671, - 358758, - 358832, - 358915, - 355061, - 356690, - 356726, - 356934, - 351757, - 351811, - 353875, - 353652, - 349485, - 349157, - 348864, - 350556, - 340130, - 341675, - 341800, - 343359, - 347927, - 348804, - 349494, - 349675, - 360665, - 362682, - 362551, - 361971, - 350838, - 352864, - 352740, - 352964, - 349932, - 351148, - 350571, - 352899, - 358457, - 359602, - 359179, - 361420, - 358205, - 360007, - 358761, - 360217, - 357098, - 356577, - 358762, - 358004, - 354696, - 357033, - 356561, - 357468, - 341945, - 343627, - 342694, - 343302, - 327449, - 328638, - 329643, - 328397, - 339163, - 339836, - 339889, - 339976, - 351430, - 352003, - 352779, - 353306, - 351710, - 351784, - 352444, - 352589, - 354325, - 355971, - 355766, - 355392 - ], - "p50Micros": 353306, - "p95Micros": 362682, - "minMicros": 176769, - "maxMicros": 367994, - "wallMicros": 8769924, - "operationsPerSecond": 22.80521473162139 - }, - "orm": { - "samplesMicros": [ - 184513, - 185387, - 186333, - 186942, - 362417, - 363332, - 364999, - 365266, - 356960, - 356322, - 357832, - 357161, - 357644, - 355703, - 356700, - 359003, - 358124, - 358178, - 360667, - 362429, - 351307, - 350875, - 352394, - 353484, - 335627, - 337773, - 337144, - 335725, - 339210, - 339879, - 340357, - 341677, - 352103, - 353099, - 354311, - 354896, - 357140, - 358966, - 358277, - 358174, - 358533, - 360042, - 359285, - 358992, - 360937, - 362317, - 363788, - 365158, - 365445, - 367836, - 366846, - 367810, - 360439, - 360093, - 360481, - 362287, - 360810, - 362265, - 363217, - 363024, - 355454, - 354649, - 353349, - 354265, - 337898, - 339302, - 338963, - 340469, - 336412, - 336839, - 337220, - 337642, - 340588, - 341918, - 341832, - 342843, - 354345, - 354927, - 356237, - 356455, - 357305, - 358985, - 357543, - 358718, - 352232, - 351558, - 352799, - 355059, - 357900, - 356712, - 358072, - 359133, - 363011, - 365130, - 364121, - 365945, - 363094, - 365926, - 364967, - 366621, - 363990, - 365941, - 364397, - 365604, - 358066, - 357279, - 358773, - 360597, - 352039, - 350601, - 350337, - 352055, - 338636, - 339236, - 340749, - 340135, - 335789, - 337048, - 338146, - 337575, - 354948, - 355870, - 356201, - 357132, - 365564, - 365465, - 365514, - 365561, - 360429, - 361710, - 362765, - 363940, - 362418, - 362669, - 365326, - 365057, - 359453, - 361470, - 360415, - 362158, - 358745, - 361477, - 361239, - 361303, - 365599, - 365169, - 366896, - 364987, - 363625, - 362674, - 363991, - 365218, - 361487, - 361909, - 362953, - 362848, - 341256, - 342763, - 343952, - 342321, - 329448, - 329074, - 330049, - 329532, - 345679, - 346460, - 347693, - 348166, - 353003, - 352449, - 354763, - 354082, - 349852, - 348385, - 349722, - 351055, - 351764, - 351404, - 351359, - 352872, - 355624, - 354270, - 355494, - 356686, - 356779, - 356342, - 358467, - 357857, - 357763, - 357174, - 357343, - 359213, - 362400, - 363634, - 362758, - 363683, - 363586, - 365667, - 364067, - 366160 - ], - "p50Micros": 357305, - "p95Micros": 365604, - "minMicros": 184513, - "maxMicros": 367836, - "wallMicros": 8886237, - "operationsPerSecond": 22.506714596966074 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 248, - 293, - 298, - 304, - 162523, - 162652, - 162772, - 162889, - 163200, - 163245, - 163305, - 163349, - 177366, - 177640, - 177883, - 178386, - 173936, - 173913, - 173930, - 173859, - 175324, - 175330, - 175376, - 175771, - 182884, - 182659, - 182471, - 182275, - 181333, - 181448, - 181638, - 181688, - 180320, - 180045, - 179808, - 179781, - 177176, - 177140, - 177073, - 176912, - 178723, - 178697, - 178612, - 178642, - 176407, - 176525, - 176729, - 176915, - 177069, - 176786, - 176580, - 176355, - 163604, - 163434, - 163317, - 163181, - 169536, - 170809, - 171248, - 171657, - 176478, - 175444, - 175245, - 175408 - ], - "p50Micros": 176355, - "p95Micros": 182275, - "minMicros": 248, - "maxMicros": 182884, - "wallMicros": 10465844 - }, - "sqlMicros": [ - 81439, - 81503, - 81595, - 81701, - 80425, - 80698, - 80993, - 80807, - 79787, - 79987, - 80163, - 79808, - 82792, - 83090, - 83205, - 83522, - 84080, - 84279, - 84328, - 84368, - 91643, - 92903, - 93718, - 93618, - 81095, - 81710, - 80796, - 80227, - 91563, - 92115, - 92872, - 93172, - 84156, - 84106, - 85231, - 85962, - 88861, - 90007, - 89977, - 90844, - 85945, - 85573, - 84357, - 85383, - 96006, - 96521, - 97077, - 97692, - 88400, - 88272, - 88065, - 89406, - 91974, - 92317, - 92502, - 93231, - 84782, - 84584, - 85763, - 86469, - 93852, - 94046, - 94704, - 94509, - 86229, - 85418, - 86349, - 86184, - 90313, - 90452, - 91171, - 90876, - 86345, - 86156, - 87175, - 86777, - 91707, - 91439, - 92049, - 92039, - 83062, - 84100, - 83777, - 84138, - 91621, - 92509, - 92741, - 92842, - 85056, - 86189, - 84995, - 86006, - 90200, - 90819, - 90875, - 91442, - 80169, - 80000, - 80680, - 80472, - 82698, - 82888, - 83067, - 83108, - 81318, - 81619, - 81378, - 81589, - 87336, - 88446, - 89428, - 89935, - 81906, - 80186, - 79996, - 81226, - 94100, - 93953, - 94765, - 94545, - 85248, - 86052, - 87307, - 87044, - 88681, - 89816, - 89976, - 89952 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 246, - 359, - 388, - 413, - 178874, - 179622, - 180287, - 180820, - 183570, - 183475, - 183456, - 183554, - 181230, - 180966, - 180889, - 180877, - 184512, - 184653, - 184740, - 184747, - 181906, - 181688, - 181559, - 181904, - 183268, - 183530, - 183643, - 183406, - 180191, - 181227, - 181471, - 181301, - 167623, - 165044, - 164235, - 163859, - 170035, - 170146, - 170242, - 170389, - 181608, - 182020, - 182351, - 182627, - 180000, - 179735, - 179685, - 179584, - 179450, - 179463, - 179470, - 179666, - 180771, - 180623, - 180503, - 180360, - 179748, - 179836, - 179977, - 180089, - 178552, - 178777, - 179559, - 179518 - ], - "p50Micros": 180287, - "p95Micros": 184512, - "minMicros": 246, - "maxMicros": 184747, - "wallMicros": 10768317 - }, - "sqlMicros": [ - 84451, - 85123, - 85719, - 86081, - 92041, - 93847, - 93068, - 94658, - 85966, - 85210, - 85175, - 86810, - 94993, - 97253, - 96911, - 98004, - 87823, - 87739, - 86900, - 87920, - 92394, - 91724, - 93005, - 93205, - 86732, - 86912, - 86186, - 87178, - 95577, - 97393, - 96687, - 98215, - 84203, - 85476, - 87294, - 87017, - 94168, - 94421, - 94505, - 96377, - 85375, - 85408, - 84906, - 87341, - 95205, - 97007, - 97372, - 97268, - 85302, - 83720, - 85507, - 85249, - 93087, - 94428, - 95559, - 96917, - 82726, - 81964, - 84507, - 81635, - 81494, - 81953, - 81994, - 82052, - 80991, - 81335, - 81153, - 81142, - 88295, - 88468, - 89061, - 89074, - 85186, - 85499, - 86760, - 86691, - 94271, - 94731, - 94952, - 96972, - 84863, - 83986, - 85043, - 86060, - 92288, - 93951, - 94986, - 94449, - 87071, - 87127, - 87017, - 87006, - 89915, - 91983, - 91726, - 92661, - 86123, - 85583, - 85404, - 87060, - 92720, - 92853, - 94272, - 95207, - 85269, - 85373, - 85347, - 85291, - 92849, - 93234, - 93458, - 95313, - 83965, - 83312, - 83163, - 84739, - 92978, - 94299, - 95173, - 95316, - 85307, - 88702, - 88267, - 87298, - 96847, - 96058, - 96936, - 97957 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 171 - ], - "sqlMicros": [ - 79809, - 85569 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"id\", \"t0\".\"email\" FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) ORDER BY \"t0\".\"id\" ASC", - "parameters": [ - 10001 - ] - }, - { - "sql": "SELECT \"c0\", \"c1\", \"c2\" FROM (SELECT \"t0\".\"id\" AS \"c0\", \"t0\".\"title\" AS \"c1\", \"t0\".\"author_id\" AS \"c2\", ROW_NUMBER() OVER (PARTITION BY \"t0\".\"author_id\" ORDER BY \"t0\".\"id\" DESC) AS \"c3\" FROM \"posts\" AS \"t0\" WHERE (\"t0\".\"author_id\" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100))) AS \"orm_partition\" WHERE \"c3\" > $101 AND \"c3\" <= $102 ORDER BY \"c3\"", - "parameters": [ - 10001, - 10002, - 10003, - 10004, - 10005, - 10006, - 10007, - 10008, - 10009, - 10010, - 10011, - 10012, - 10013, - 10014, - 10015, - 10016, - 10017, - 10018, - 10019, - 10020, - 10021, - 10022, - 10023, - 10024, - 10025, - 10026, - 10027, - 10028, - 10029, - 10030, - 10031, - 10032, - 10033, - 10034, - 10035, - 10036, - 10037, - 10038, - 10039, - 10040, - 10041, - 10042, - 10043, - 10044, - 10045, - 10046, - 10047, - 10048, - 10049, - 10050, - 10051, - 10052, - 10053, - 10054, - 10055, - 10056, - 10057, - 10058, - 10059, - 10060, - 10061, - 10062, - 10063, - 10064, - 10065, - 10066, - 10067, - 10068, - 10069, - 10070, - 10071, - 10072, - 10073, - 10074, - 10075, - 10076, - 10077, - 10078, - 10079, - 10080, - 10081, - 10082, - 10083, - 10084, - 10085, - 10086, - 10087, - 10088, - 10089, - 10090, - 10091, - 10092, - 10093, - 10094, - 10095, - 10096, - 10097, - 10098, - 10099, - 10100, - 0, - 3 - ] - } - ], - "returnedRows": [ - 100, - 300 - ], - "driverRowsUtf8JsonBytes": [ - 3393, - 46141 - ], - "outputUtf8JsonBytes": 47933, - "acquireMicros": [ - 125 - ], - "sqlMicros": [ - 82743, - 85532 - ], - "ormDecodeMicros": [ - 407, - 59 - ], - "postgresProtocolBytes": { - "sent": 2427, - "received": 56982 - } - } - } - }, - { - "case": "aggregate", - "latency": { - "raw": { - "samplesMicros": [ - 81405, - 80486, - 82300, - 84842, - 78838, - 79375, - 79587, - 80040, - 81425, - 80180, - 79256, - 80756, - 83484, - 78335, - 81086, - 81057, - 74458, - 79624, - 79522, - 81191, - 81027, - 80982, - 82206, - 81306, - 80417, - 84001, - 82161, - 81228, - 79542, - 81504, - 84324, - 80323, - 79364, - 82385, - 82283, - 79971, - 82294, - 78138, - 82722, - 82119, - 81585, - 82335, - 81866, - 74981, - 82063, - 80026, - 79091, - 77007, - 76345, - 78031, - 79739, - 79307, - 81388, - 83074, - 81031, - 77776, - 80820, - 83383, - 77610, - 81415, - 76841, - 81690, - 80228, - 82579, - 81139, - 80420, - 78770, - 79780, - 78979, - 79675, - 82236, - 79895, - 80598, - 79276, - 80084, - 80907, - 81539, - 78855, - 83278, - 80034, - 77108, - 78744, - 74124, - 80340, - 79384, - 75161, - 80535, - 83798, - 77533, - 81072, - 82037, - 79226, - 80273, - 81159, - 83332, - 83785, - 82556, - 81105, - 79095, - 80249, - 82433, - 82361, - 81687, - 80593, - 82474, - 79911, - 80577, - 78009, - 80108, - 78051, - 80069, - 83987, - 82396, - 80230, - 83565, - 83463, - 80938, - 76126, - 78911, - 78084, - 78028, - 82251, - 81352, - 80920, - 81803, - 80312, - 82740, - 80921, - 80737, - 79584, - 79454, - 82684, - 81895, - 77650, - 80469, - 80976, - 78808, - 80490, - 81934, - 82529, - 80480, - 81211, - 78487, - 81979, - 81571, - 78739, - 78763, - 77751, - 80412, - 82698, - 82351, - 79303, - 80660, - 79080, - 81126, - 81518, - 80102, - 79282, - 77649, - 82758, - 77956, - 79060, - 81463, - 82672, - 82425, - 81027, - 83863, - 79750, - 78522, - 82952, - 80436, - 77009, - 81364, - 80571, - 80565, - 78739, - 84700, - 81798, - 81766, - 79354, - 80174, - 83319, - 83118, - 82304, - 80749, - 83235, - 76573, - 80512, - 81088, - 82133, - 81036, - 79420, - 79286, - 81790, - 83423, - 78867, - 81023, - 81556, - 82286, - 77243 - ], - "p50Micros": 80660, - "p95Micros": 83463, - "minMicros": 74124, - "maxMicros": 84842, - "wallMicros": 16112393, - "operationsPerSecond": 12.412805472160466 - }, - "orm": { - "samplesMicros": [ - 82883, - 80719, - 83098, - 80563, - 79622, - 80228, - 80058, - 80297, - 81076, - 78524, - 79542, - 79474, - 82006, - 79831, - 80692, - 81989, - 83080, - 84587, - 79565, - 80744, - 83936, - 80961, - 80854, - 83443, - 81718, - 80580, - 81015, - 80149, - 79719, - 83925, - 80535, - 78314, - 81064, - 78567, - 82298, - 79853, - 81365, - 82563, - 79874, - 78609, - 80982, - 82533, - 79363, - 79550, - 80474, - 80909, - 81772, - 78263, - 76298, - 80656, - 78265, - 82724, - 80543, - 81709, - 78475, - 79583, - 80477, - 80163, - 80265, - 81293, - 78767, - 76727, - 78627, - 77691, - 83951, - 82624, - 82879, - 80557, - 81118, - 83310, - 83369, - 84166, - 80725, - 80569, - 82677, - 80244, - 77893, - 79666, - 78673, - 83167, - 77509, - 80252, - 78571, - 80804, - 76428, - 80764, - 79000, - 83474, - 83571, - 77576, - 81413, - 80816, - 79412, - 80021, - 78407, - 80210, - 81883, - 74816, - 78943, - 77909, - 80685, - 78717, - 78222, - 81732, - 80066, - 80072, - 78794, - 78493, - 80356, - 85585, - 81675, - 81454, - 82028, - 83022, - 82510, - 82616, - 74213, - 80059, - 81762, - 80423, - 76862, - 81689, - 78522, - 81560, - 78549, - 81486, - 80765, - 80884, - 82272, - 81650, - 78856, - 81184, - 82808, - 82272, - 82878, - 80682, - 80136, - 82051, - 84049, - 80351, - 79333, - 83546, - 79825, - 81046, - 81924, - 80152, - 82073, - 81154, - 82547, - 76608, - 81189, - 79535, - 82491, - 83228, - 79428, - 79990, - 80396, - 79131, - 79403, - 82757, - 79502, - 77854, - 83355, - 79021, - 82220, - 74802, - 76673, - 77098, - 81748, - 82435, - 79571, - 82193, - 84814, - 79132, - 78753, - 80556, - 81360, - 83744, - 77412, - 80347, - 79624, - 80432, - 81402, - 80994, - 83647, - 77345, - 81862, - 79894, - 83488, - 80761, - 82299, - 82440, - 80269, - 74424, - 80414, - 78019, - 79318, - 83713, - 83942, - 78582 - ], - "p50Micros": 80563, - "p95Micros": 83713, - "minMicros": 74213, - "maxMicros": 85585, - "wallMicros": 16114601, - "operationsPerSecond": 12.411104686985423 - } - }, - "concurrent": { - "raw": { - "samplesMicros": [ - 81336, - 81510, - 81648, - 81764, - 164178, - 164363, - 164554, - 164726, - 166252, - 166793, - 166549, - 166546, - 166870, - 167701, - 167653, - 168037, - 166555, - 166992, - 166917, - 167634, - 165483, - 165401, - 166236, - 165580, - 165550, - 165583, - 165605, - 166031, - 164865, - 165237, - 164776, - 164626, - 159957, - 160410, - 159762, - 160756, - 158920, - 159063, - 158987, - 158993, - 162559, - 162972, - 163156, - 163484, - 164748, - 165083, - 165426, - 165707, - 165062, - 165561, - 165285, - 166301, - 167569, - 168226, - 167860, - 169076, - 168292, - 169247, - 169039, - 168994, - 163977, - 164684, - 164660, - 165349, - 164759, - 164838, - 165973, - 165056, - 163062, - 163659, - 163183, - 163553, - 156955, - 157083, - 157614, - 158087, - 160425, - 161498, - 161494, - 161511, - 164768, - 164836, - 165461, - 165032, - 163806, - 163707, - 163613, - 163578, - 165056, - 164971, - 165346, - 165943, - 168241, - 168341, - 168912, - 169415, - 164918, - 165068, - 165925, - 165853, - 163582, - 164121, - 164776, - 164673, - 166081, - 166136, - 166829, - 167331, - 167568, - 168079, - 168620, - 168331, - 177870, - 177944, - 177724, - 178129, - 183504, - 184008, - 184266, - 184628, - 174971, - 175251, - 175338, - 175331, - 164612, - 164734, - 164677, - 164908, - 161791, - 161564, - 162511, - 161420, - 160620, - 160522, - 160645, - 160644, - 160618, - 160987, - 161372, - 161484, - 163605, - 164030, - 164132, - 164354, - 164854, - 165354, - 165950, - 165299, - 158836, - 159777, - 159698, - 159669, - 156853, - 157782, - 157708, - 157685, - 164074, - 163934, - 163951, - 164542, - 163528, - 163500, - 163489, - 163503, - 161472, - 162041, - 161665, - 161582, - 163157, - 163197, - 163208, - 163184, - 164557, - 165185, - 164881, - 165333, - 166143, - 165934, - 166330, - 166139, - 162983, - 163067, - 163081, - 163916, - 164918, - 164521, - 165074, - 165068, - 166964, - 166748, - 166713, - 167274, - 166052, - 166150, - 166749, - 166286, - 165580, - 165451, - 165364, - 165322 - ], - "p50Micros": 164854, - "p95Micros": 175251, - "minMicros": 81336, - "maxMicros": 184628, - "wallMicros": 4128903, - "operationsPerSecond": 48.43901636827022 - }, - "orm": { - "samplesMicros": [ - 80509, - 81092, - 81259, - 81532, - 161271, - 161746, - 161968, - 162249, - 164696, - 164408, - 164959, - 164657, - 165869, - 167069, - 166361, - 167152, - 167327, - 167331, - 167198, - 167684, - 167307, - 167052, - 166838, - 166680, - 162858, - 162598, - 163452, - 162445, - 157078, - 157254, - 157517, - 157201, - 155887, - 156113, - 156031, - 156047, - 159597, - 160199, - 160872, - 161026, - 164160, - 164582, - 164999, - 165410, - 167748, - 168503, - 169951, - 169489, - 166948, - 167410, - 168293, - 169049, - 163667, - 165069, - 164896, - 164852, - 165832, - 166764, - 166712, - 168206, - 167187, - 168135, - 168139, - 169223, - 163766, - 163524, - 164222, - 165068, - 166861, - 166871, - 166496, - 167124, - 167935, - 168667, - 169432, - 168748, - 167355, - 166869, - 167733, - 167709, - 164143, - 165143, - 165879, - 165375, - 164787, - 164897, - 165720, - 166424, - 167811, - 167621, - 168173, - 167866, - 161857, - 162504, - 163389, - 163258, - 165252, - 164936, - 165534, - 165271, - 168420, - 169246, - 168779, - 168766, - 165746, - 166925, - 166745, - 166477, - 166010, - 166156, - 166184, - 167328, - 164984, - 165793, - 165471, - 164968, - 161710, - 161570, - 161377, - 162395, - 160515, - 160776, - 160612, - 160585, - 160162, - 160382, - 160624, - 160552, - 159422, - 159597, - 159777, - 159595, - 160744, - 160786, - 160830, - 160874, - 161955, - 162760, - 163192, - 163303, - 163215, - 163867, - 163886, - 164043, - 162553, - 163278, - 163864, - 164829, - 163613, - 163792, - 163809, - 165053, - 166044, - 166762, - 167515, - 166799, - 167048, - 167678, - 168649, - 168420, - 166941, - 166645, - 167267, - 167204, - 167153, - 167417, - 168171, - 168824, - 169083, - 168744, - 170313, - 169832, - 166370, - 167666, - 168444, - 169429, - 167948, - 169361, - 168789, - 169278, - 165145, - 167244, - 165558, - 166976, - 162231, - 162934, - 162402, - 162754, - 163183, - 163997, - 165358, - 165001, - 162665, - 162348, - 162745, - 163172, - 160633, - 160374, - 159818, - 160162 - ], - "p50Micros": 165069, - "p95Micros": 169083, - "minMicros": 80509, - "maxMicros": 170313, - "wallMicros": 4118426, - "operationsPerSecond": 48.562241982738065 - } - }, - "observedContention": { - "raw": { - "acquisition": { - "samplesMicros": [ - 185, - 159, - 162, - 165, - 83692, - 83934, - 84121, - 84327, - 85482, - 85398, - 85371, - 85394, - 81080, - 81051, - 81058, - 81051, - 81294, - 81226, - 81379, - 81441, - 82357, - 82117, - 81825, - 81646, - 80383, - 80363, - 80350, - 80339, - 79159, - 79124, - 79143, - 79145, - 81646, - 81850, - 81953, - 82068, - 84063, - 83941, - 84215, - 84480, - 79930, - 79872, - 79623, - 79422, - 81716, - 81693, - 81671, - 81697, - 82753, - 82680, - 82653, - 82657, - 82770, - 82757, - 82791, - 82844, - 79985, - 79891, - 79872, - 79877, - 85071, - 84945, - 84877, - 84822 - ], - "p50Micros": 81693, - "p95Micros": 85371, - "minMicros": 159, - "maxMicros": 85482, - "wallMicros": 4925006 - }, - "sqlMicros": [ - 83402, - 83720, - 83950, - 84182, - 84822, - 84885, - 85475, - 85934, - 80236, - 81165, - 80970, - 81327, - 80624, - 81124, - 81679, - 81301, - 81954, - 81716, - 81513, - 82378, - 80293, - 80305, - 80277, - 80407, - 78999, - 79098, - 79212, - 79118, - 81359, - 81808, - 81853, - 82033, - 83345, - 83514, - 84225, - 84901, - 79718, - 79745, - 79502, - 79266, - 80901, - 81574, - 81582, - 82178, - 82507, - 82590, - 82375, - 82739, - 82149, - 82861, - 82521, - 83070, - 79062, - 79588, - 79976, - 80445, - 84693, - 84693, - 84605, - 85344, - 81868, - 81872, - 81840, - 82408 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - }, - "orm": { - "acquisition": { - "samplesMicros": [ - 318, - 485, - 540, - 582, - 83044, - 83434, - 83711, - 83966, - 82546, - 82683, - 82685, - 82710, - 82463, - 82211, - 82145, - 82140, - 84731, - 84724, - 84728, - 84776, - 82455, - 82324, - 82323, - 82339, - 83106, - 83043, - 83011, - 83037, - 85731, - 85714, - 85717, - 85724, - 83740, - 83650, - 83902, - 83958, - 82429, - 82318, - 82066, - 82070, - 84988, - 84983, - 85244, - 85303, - 85020, - 84869, - 84580, - 84446, - 82651, - 82711, - 82746, - 82840, - 80868, - 80560, - 80337, - 80138, - 80718, - 80651, - 80645, - 80619, - 79945, - 79909, - 79911, - 79913 - ], - "p50Micros": 82685, - "p95Micros": 85714, - "minMicros": 318, - "maxMicros": 85731, - "wallMicros": 4977874 - }, - "sqlMicros": [ - 82322, - 82994, - 83072, - 83461, - 81513, - 81929, - 83157, - 83069, - 81556, - 82546, - 81997, - 81962, - 84336, - 84544, - 84590, - 84579, - 81171, - 82145, - 82770, - 82431, - 82164, - 83179, - 83139, - 82859, - 84517, - 85303, - 86116, - 86059, - 83275, - 82965, - 83982, - 84027, - 80934, - 82143, - 81900, - 83011, - 83797, - 84810, - 85624, - 85398, - 83473, - 84713, - 85263, - 84581, - 82083, - 82345, - 82430, - 83346, - 80264, - 80192, - 79968, - 80896, - 80492, - 80699, - 80612, - 80584, - 79861, - 79877, - 79841, - 79925, - 79740, - 79832, - 79756, - 79747 - ], - "scope": "Separate instrumented eight-client run; acquisition includes driver lease/setup, not only native pool queue time." - } - }, - "probes": { - "raw": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 281 - ], - "sqlMicros": [ - 81133 - ], - "ormDecodeMicros": [], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - }, - "orm": { - "sql": [ - { - "sql": "SELECT \"t0\".\"score\", COUNT(\"t0\".\"id\") FROM \"users\" AS \"t0\" WHERE (\"t0\".\"id\" >= $1) GROUP BY \"t0\".\"score\" ORDER BY \"t0\".\"score\" ASC", - "parameters": [ - 10001 - ] - } - ], - "returnedRows": [ - 10 - ], - "driverRowsUtf8JsonBytes": [ - 71 - ], - "outputUtf8JsonBytes": 71, - "acquireMicros": [ - 172 - ], - "sqlMicros": [ - 77320 - ], - "ormDecodeMicros": [ - 16 - ], - "postgresProtocolBytes": { - "sent": 244, - "received": 413 - } - } - } - } - ] - } - ] -} diff --git a/research/dart_migration_forms.md b/research/dart_migration_forms.md deleted file mode 100644 index ef2419ea..00000000 --- a/research/dart_migration_forms.md +++ /dev/null @@ -1,161 +0,0 @@ -# 全 Dart schema 与迁移形态研究 - -研究日期:2026-09-15。基于 `next` 的 `b694ba9`;本轮只增加研究文档与隔离原型,没有更改 ORM 公开 API、生成器或 CLI。 - -实现跟进:全 Dart 工作流已实现,并修正为一套历史固定一个数据库。实际命令与文件组织见 -[迁移文档](../doc/migrations.md)。实现选择把当前物理结构单独输出为 -`schema.snapshot.dart`,使迁移工具不必加载业务模型或 Flutter 依赖;下文保留原始研究记录。 - -## 结论 - -可以让 ORM 的 schema、生成客户端、历史结构、迁移、驱动配置和迁移入口全部以 Dart 文件交付,不要求应用维护 JSON 文件。推荐默认形态是 **Dart 声明 + 自动生成的固定迁移值 + 独立历史描述 + 静态导入的运行入口**。 - -需要改变的是工具的输入输出与装载方式,不需要因为扩展名改变而重写事务、数据库驱动或恢复机制。当前运行器已经接受 `List`,JSON 主要是生成器和 CLI 的文件协议。 - -这里的“全 Dart”指 ORM 的声明和版本化工件。SQL 可以写在 Dart 多行字符串中;数据库仍有迁移记录和 checkpoint;内部校验和编码仍可使用 JSON。它不要求改造 Dart 自身的 pubspec/build 配置格式,也不要求为消除内部 `jsonEncode` 再造编码协议。 - -## 三种不同的产品形态 - -| 形态 | 开发者体验 | 需要承担的代价 | 判断 | -| --- | --- | --- | --- | -| `up(db)` / `down(db)` 执行回调 | 普通 Dart,可调用函数、循环、await | 任意代码无法在不执行它的情况下得到完整计划;闭包与外部依赖无法用函数对象可靠校验;恢复语义需要另外定义 | 保留为未来明确的高级能力,不作为默认 | -| `Migration.steps(...)` 声明操作 | 补全、参数检查、注释、SQL 多行字符串;可由 diff 自动生成 | 需要稳定的操作模型与历史结构;SQL 本身不因此获得 Dart 字段类型检查 | 推荐默认,最接近现有执行器 | -| 每次运行 `Migration.diff(old, current)` | 表面最少文件 | 缺少固定迁移历史;新版 diff 算法或当前模型会改变旧计划,数据变换也不能由结构推断 | diff 只在创建迁移时运行 | - -“构建器回调”可以作为第二种的语法糖:它只构造步骤,没有数据库会话;冻结后的步骤才进入运行器。它与执行 `await db.execute(...)` 的回调是不同协议。不要依靠试跑用户回调来发现 SQL,也不应为这层糖扩张出通用 Dart 解释器。 - -## 默认用户过程 - -1. 修改 `schema.dart`。 -2. 生成当前客户端;创建迁移时比较上一份历史快照与当前结构。 -3. 工具输出新的 Dart 迁移文件,开发者审阅、补充 SQL 或恢复步骤。 -4. 检查迁移链和旧版本升级,再把文件提交到版本库。 -5. 服务端独立运行迁移入口;Flutter 在使用业务数据库前调用相同迁移列表。 - -普通新增列仍应自动生成。全 Dart 不代表让用户重新手写每条 CREATE/ALTER,也不要求用户重复维护 Record 和一份列 DSL。 - -建议首先验证这个文件布局: - -```text -lib/data/ - schema.dart - schema.orm.dart - migrations/ - migrations.g.dart - m0001_initial.dart - m0002_nicknames.dart -tool/ - migrate.dart -``` - -- `schema.dart` 是当前业务声明;`schema.orm.dart` 是可重新生成的当前客户端和结构描述。 -- 每个 `mNNNN_*.dart` 初次由工具生成,保存固定步骤和当时的历史结构,审阅后成为版本化源码,不随当前 schema 重新生成。 -- 默认把历史描述放在同一迁移文件的私有区域,避免每次迁移固定新增两三个辅助文件。需要历史字段补全的复杂回填再评估是否值得单独生成辅助库。 -- `migrations.g.dart` 只负责静态 import 和有序注册。清空构建缓存后,可从已提交的迁移文件恢复;它不存放唯一一份历史事实。 -- `tool/migrate.dart` 负责选择驱动、读取配置和连接资源。应用持有的迁移定义不捕获密码或数据库实例。 - -除 Dart 模型、迁移代码和必要的项目配置之外,不增加需要人手同时维护的事实来源。历史结构包含重复数据是正常的;它代表另一个时间点。 - -## 可实现的迁移文件 - -下列使用当前 `Migration.steps` API;校验和与 `_after` 的内容为展示省略,完整文件见 `example/migrations/`: - -```dart -final migration = Migration.steps( - '0002_nicknames', - [ExecuteSql('ALTER TABLE "people" ADD COLUMN "nickname" TEXT')], - dialect: .sqlite, - previous: '创建迁移时固定的前驱校验和', - snapshot: _after, -); - -// 工具生成的历史结构;不导入当前 schema.orm.dart。 -final _after = SchemaSnapshot([ - TableSchema( - 'people', - columns: [ - Column('id', Codecs.integer), - Column('name', Codecs.text), - Column('nickname', Codecs.text.nullable(), nullable: true), - ], - primaryKey: ['id'], - ), -]); -``` - -这些 `Column` 提供构造参数的 Dart 类型检查;字符串列名、SQL 表达式及迁移阶段是否已存在该列仍需计划校验和真实数据库验证。不能把 `.dart` 扩展名等同于完整迁移类型安全。 - -`Migration.create/diff` 可在生成阶段调用,但保存的普通 DDL 应固定为步骤;应用启动不根据目标快照重新推导旧 SQL。重建、回填等结构化操作仍依赖运行器,所以还必须明确操作协议的版本和升级兼容测试。纯 JSON 也有同样的运行器依赖。 - -## 历史类型和高级回填 - -若用户要在迁移中写类型化字段选择,需要的是 **该迁移版本的字段描述**,不能借用最新生成客户端。比如 v1 有 `name`,v2 新增 `nickname`,v3 删除 `name`,v1 → v2 的代码必须继续存在且可编译。 - -Drift 的官方文档明确展示了这个问题,并提供每一步对应版本的生成 schema;这是有用的验证对象,不是要求复制其文件组织。[历史 schema 与逐步迁移](https://drift.simonbinder.eu/migrations/step_by_step/) - -可以进一步研究 `before.people.name`、`after.people.nickname` 这样的历史符号,以及 `copy` / `rename` / `backfill` 的类型化操作。但是,仅有 before/after 两个类型不能证明中间执行阶段:新增列必须先执行,随后才能写该列。需要步骤作用域或运行器检查;不建议为此先引入大量状态泛型。 - -普通批量 SQL 更新优先保留现在的结构化 `Backfill`:不可变主键游标、批次数据与 checkpoint 同事务、完成条件单独检查。任意 Dart 数据变换则要额外定义: - -- 读取和写入哪些历史字段、codec 如何冻结; -- 批次边界、恢复位置,以及提交未知时如何确认; -- 是否允许调用当前业务函数、网络服务和有副作用的代码; -- 修改回调依赖后如何识别历史变化。 - -最后一点没有“对函数调用 toString 后哈希”这样的可靠通用解法。若接受任意回调,只能进一步约束依赖、记录源码/构建指纹并要求明确版本与恢复规则,不能沿用“纯计划已完整校验”的承诺。 - -## CLI 和 AOT 的实际形态 - -全 Dart 文件需要一个编译入口。推荐先提供项目自己的普通入口,静态 import 迁移列表,再调用迁移运行器。`runMigrationCli` 已实现,入口形态为: - -```dart -import '../lib/data/migrations/migrations.g.dart'; - -Future main(List args) => runMigrationCli( - args, - history: migrationHistory, - directory: 'lib/data/migrations', - connect: ({required readOnly}) => postgres(PostgresOptions(url: configuredUrl)), -); -``` - -命令是 `dart run tool/migrate.dart plan|apply|check`。先检查静态计划,再根据命令决定是否连接;迁移定义本身不取得数据库会话。生成当前 schema 继续走独立的生成命令,避免“生成器入口必须先导入还没生成的文件”的循环。 - -开发环境可以编译临时入口自动 import 文件,但首版没有必要额外引入进程调度/序列化桥接。生产可编译这个项目入口;Flutter 直接静态导入列表。Dart 官方支持编译独立 executable;语言 import 本身使用声明中的 URI。[dart compile](https://dart.dev/tools/dart-compile)、[Libraries & imports](https://dart.dev/language/libraries) - -因此不承诺一个已编译的通用 ORM 程序能够运行时扫描任意新 `.dart` 文件并执行其中的迁移。静态生成注册表是装载方式,不需要反射、宏或运行时源码解释器。 - -## 校验与兼容 - -继续对固定步骤、历史结构、前驱和格式版本做内容校验,不对 Dart 格式化和注释本身做数据库历史校验。普通注释变动不应要求重放迁移;SQL 字符串的变动仍应保守地视为内容变化。 - -前驱校验和应是创建迁移时保存的字面量。若写成 `previous: old.checksum`,修改老文件会同时改变新文件的链引用,削弱离线识别历史变化的能力。应用数据库里的已应用记录仍是独立校验来源,但不应只靠它发现问题。 - -需要进一步设计冻结记录或发布指纹,识别尚未执行到当前数据库的历史文件变动,以及结构化执行器语义升级。仅校验当前对象不能证明任意 Dart 声明无环境依赖;工具可以检查生成文件的受限声明形式,但那不是 Dart 自带保证。 - -使用 `const` 有利于纯数据和不可变值,官方也明确了常量构造约束;当前 `Migration` / `SchemaSnapshot` 不是 const,不能直接在示例里加关键字冒充已支持。默认先复用不可变对象,不为追求所有值 const 重写一套类型。[Dart 常量构造](https://dart.dev/language/constructors#constant-constructors) - -## 实证与下一步范围 - -早期原型在 `37aec68` 验证了 Dart 3.13.3、SQLite/PostgreSQL 下 JIT 与独立 AOT 每后端 8 项检查。其双库计划形态已淘汰,原型源码由正式的 `test/migration_source_test.dart`、`test/migration_target_test.dart` 和恢复测试替代;旧运行结果不作为当前实现的验收。 - -## 数据库边界修正 - -ORM 支持多个数据库,与应用需要多套迁移是两件不同的事。应用选择数据库之后,每个注册表从空历史开始固定 `migrationDialect`。迁移文件保存同一 `dialect` 和一组平铺的步骤;初始化时配置一次,正常创建及执行不再询问或遍历其他数据库。 - -| 边界 | 行为 | -| --- | --- | -| 生成 | 当前声明与最后历史快照都按已选数据库解析,再比较并冻结 SQL。只要求该库的转换表达式。 | -| 指纹 | 包含数据库身份、实际步骤、选定表达式的历史 schema、前驱。另一库配置变化不能改变当前指纹。 | -| 注册 | 只接受同一库的有序历史。重建注册表保留已选数据库,不刷新文件指纹。 | -| 连接 | CLI 和执行器在迁移 SQL 前拒绝错库;CLI 的连接工厂已执行,可能已打开 SQLite 文件,不能声称零文件系统副作用。 | -| 数据库版本 | 当前迁移执行器支持 PostgreSQL 18+、SQLite 3.35+。执行前检查实际版本;不推测手写 SQL 对所有版本的兼容性。 | -| 特有能力 | SQLite 重建/外键检查与 PostgreSQL 约束解析/并发索引各走本库执行语义,不为另一库生成占位步骤或采用功能交集。 | -| 事务 | 普通迁移批次原子提交。显式 Backfill/CheckedSql 分段记账;失败及重试遵循已保存检查点,不把整个迁移描述为可回滚。 | -| 改连接地址 | 同一数据库类型可以切环境、凭据、服务器,仍核对目的库的已应用历史及实际结构。 | -| 真正换库 | 单独的数据搬迁及新库 baseline。原库历史不改写、不自动翻译或重放。 | -| 同时使用多个库 | 分别维护连接、目录、注册表与发布动作;没有跨库原子迁移承诺。 | - -SQLite 可在虚拟计算列上建索引,PostgreSQL 18 不支持这种索引;这个限制不能阻止 SQLite schema。PostgreSQL 对计算列模式转换的限制也不能阻止可通过复制重建完成的 SQLite 变更。[PostgreSQL generated columns](https://www.postgresql.org/docs/18/ddl-generated-columns.html)、[SQLite generated columns](https://www.sqlite.org/gencol.html) - -迁移验收须使用实际部署的引擎;PostgreSQL 应用的 SQLite 测试不能替代 PostgreSQL 迁移验收。重命名、丢弃数据、重计算及长时间锁定仍需明确审阅,选择单库不会消除这些边界。 diff --git a/research/flutter-web-integration.md b/research/flutter-web-integration.md deleted file mode 100644 index 97d3fbba..00000000 --- a/research/flutter-web-integration.md +++ /dev/null @@ -1,164 +0,0 @@ -# Flutter Web integration research - -The recommendation below has since been implemented; see [current setup](../doc/sqlite-web.md) -and [refactor validation](validation/sqlite-refactor.md). The measurements in this -research note describe the earlier probe. - -Date: 2026-09-16. Recommendation: retain `sqlite3` as the engine, share its -`CommonDatabase` interfaces, and make browser resource packaging an ORM product -responsibility. Flutter applications should not author a worker or assemble -SQLite WASM URLs for the normal setup. This is a researched direction with a -working packaging probe, not a released initialization API. - -## What sqlite3 already provides - -`sqlite3` 3.6.0 supports native and browser execution, including Flutter Web. -Native code imports `sqlite3.dart`; browser code imports `wasm.dart` and loads a -compatible SQLite WASM module. Both implement the database and statement -interfaces in `common.dart`. The ORM should reuse those interfaces wherever -execution logic is identical. The package also supplies the memory, IndexedDB and -OPFS file systems; there is no reason to reimplement those in the ORM. -[Source](https://pub.dev/packages/sqlite3/versions/3.6.0). - -The package exposes synchronous database operations. An async initialization -method does not move subsequent SQL off the UI thread. Its `VfsWorker` provides -file access to a separate database worker; it is not an async SQL client with ORM -transactions, cursor ownership and query notifications. Native isolate messaging -and browser worker messaging therefore remain small internal responsibilities. -[VfsWorker](https://pub.dev/documentation/sqlite3/latest/wasm/VfsWorker-class.html). - -Flutter's `compute()` executes on the main thread on Web. It cannot replace this -database worker. SQL execution in a worker also does not eliminate main-thread -row decoding and widget work, so bounded result sets and streaming still matter. -[Flutter isolates](https://docs.flutter.dev/perf/isolates). - -## Packaging options - -| Option | Application work and cost | Decision | -| --- | --- | --- | -| Package-owned, precompiled JS worker and SQLite WASM | Flutter bundles declared assets; no application worker compilation | Preferred normal Flutter path | -| Flutter asset transformer | Automatic compilation during run/build; adds compiler invocation and dependency tracking | Optional source-build/custom-worker path; not needed for the stock worker | -| ORM setup command copies matched artifacts to `web/` | One explicit setup step and stale-file checks | Useful for plain Dart deployments or explicit web-root layouts | -| Add `sqlite_async` | Reuses an asynchronous SQLite layer; still needs worker/WASM resources | Does not itself solve this packaging problem; do not add it solely for Flutter Web | - -Flutter automatically bundles assets declared by dependencies. Since Flutter -3.41, entries can specify `platforms: [web]`. A probe confirmed that the ORM -package can retain its pure Dart dependencies while adding this metadata: - -```yaml -flutter: - assets: - - path: assets/web/ - platforms: [web] -``` - -The Flutter consumer needs no matching asset declaration. Standalone `dart pub -get --offline` also succeeds for that modified package without a Flutter SDK -dependency. An Android-only sentinel is excluded from the probe's Web output; -native application packaging was not rebuilt in this research. -[Package assets and platform filters](https://docs.flutter.dev/ui/assets/assets-and-images). - -The release process would compile the ORM's worker once and package it with the -tested SQLite module. Application schema and migration definitions stay in the -application's Dart compilation; they are not inputs to the stock worker build. -Worker and engine assets currently total 924,659 bytes, or 414,884 bytes when -individually gzip-compressed. These are artifact sizes, not measured network -transfer or application startup costs. Package downloads would still include -these files even when a consumer does not build for Web. - -Flutter's asset transformer protocol is a Dart executable accepting input/output -paths, and current tooling can track an emitted dependency file. This is viable -when a source-built worker is actually needed. Current Dart hook documentation -supports native `CodeAsset` output, not a general Web resource bundler; introducing -a hook alone would not provide the proposed integration. -[Asset transformers](https://docs.flutter.dev/ui/assets/asset-transformation), -[Dart hooks](https://dart.dev/tools/hooks). - -`sqlite_async` already solves asynchronous database execution, but its documented -Web setup still supplies worker and WASM URIs. Adopting it would additionally need -a separate review of its transaction, cursor and custom-function contracts against -the ORM's requirements. This research makes no comparative performance claim. -[sqlite_async Web setup](https://pub.dev/packages/sqlite_async#web). - -## Runtime boundaries to keep explicit internally - -1. **A single SQLite product entry.** Internally select native opening/transport - or Web opening/transport. Keep SQLite and PostgreSQL configuration separate. - Storage policy remains meaningful: a browser database name is not an OS file - path. Do not guess a native application directory inside the pure Dart core. -2. **Resolve assets at the application base.** The probe loads package resources - under `/nested/assets/packages/orm/assets/web/`, starting directly at - `/nested/detail/42`. `document.baseURI` resolves to `/nested/`, whereas - `Uri.base` includes the current route. Current `SqliteWebDriver.open` resolves - relative WASM paths with `Uri.base`, so the future default resolver must supply - correct absolute URLs. The probe does that without changing the driver. -3. **Separate worker and engine URLs for advanced deployment.** Flutter's - `assetBase` can move assets to a CDN, while a direct Worker URL must satisfy the - document's same-origin constraint. Default to same-origin resources; allow - explicit URL overrides. A CDN/blob bootstrap path needs its own CORS/CSP tests - and must not silently inject a service worker. -4. **Publish matched artifacts.** Add protocol/build identity to the startup - handshake and use versioned/content-addressed filenames. Fail clearly when - app, worker or SQLite module versions disagree. Deployment must retain assets - used by still-open older application versions. This is not implemented yet. -5. **Storage capability is independent of Flutter rendering.** Current - `SimpleOpfsFileSystem` works in a dedicated worker, owns two persistent files, - and holds exclusive access. The probe verifies it without cross-origin - isolation headers. Do not force COOP/COEP just to use this storage path, and do - not silently fall back from persistent storage to memory. -6. **Multiple tabs need a separate design.** The present second-owner rejection - remains real. Replacing the worker with `SharedWorker` does not suffice because - this VFS requires a dedicated worker. Connection sharing, change notifications, - upgrades and worker lifetime would have to be handled together. - -References: [Flutter bootstrap and assetBase](https://docs.flutter.dev/platform-integration/web/initialization), -[Worker URL restrictions](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker), -[SimpleOpfsFileSystem](https://pub.dev/documentation/sqlite3/latest/wasm/SimpleOpfsFileSystem-class.html). - -Flutter's JS/WASM application compilation is independent of SQLite's WASM engine. -Both application modes can use the same JS database worker. `flutter build web ---wasm` also emits a JS fallback; a successful build alone does not prove the page -actually used WASM. The probe records `dart.tool.dart2wasm` at runtime and tests -with and without the isolation headers used for multithreaded Flutter rendering. -[Flutter WASM](https://docs.flutter.dev/platform-integration/web/wasm). - -## Actual Flutter probe - -The original probe, now evolved into the [acceptance runner](../tool/test_flutter_web.dart), copies the current ORM -library into a disposable package, adds platform-filtered assets, compiles the -existing worker and creates a real Flutter widget application. It reuses the 20 -browser acceptance scenarios, changing only resource resolution, the reload URL, -and reporting. The long SQL scenario additionally asserts that Flutter animation -ticks advance while SQL executes. It starts after the first Flutter frame and -keeps the widget tree mounted throughout the checks. - -```sh -dart run tool/build_sqlite_web.dart --check -dart run tool/test_flutter_web.dart /absolute/path/to/flutter -``` - -On this host, commands use `DEVELOPER_DIR=/Library/Developer/CommandLineTools`. -The current runner rebuilds its own `.dart_tool/flutter_web_test/` directory, needs cached -Flutter/pub dependencies for offline resolution, serves local renderer assets, -and uses disposable Chrome profiles. It does not modify the shipping pubspec. - -| Flutter release build | Actual Dart WASM | crossOriginIsolated | Result | -| --- | --- | --- | --- | -| JS | false | false | 20 checks passed | -| WASM | true | false | 20 checks passed | -| WASM | true | true | 20 checks passed | - -Environment: Flutter 3.47.4, Dart 3.13.3, Chrome 153, sqlite3 3.6.0. -[Captured evidence](validation/flutter-web-probe.json) includes SDK revisions, -engine/worker hashes, resource requests, execution-mode flags and checks. -The checks include typed relationships/projections, transactions/savepoints, -subscriptions, streaming, precision, OPFS close/reopen, interrupted-transaction -recovery across page reload, and migration upgrades. - -This establishes release-build feasibility on Chromium with same-origin assets, -including nested deployment. It does not establish Flutter hot-restart lifecycle, -Safari/Firefox operation, JS fallback selection from a WASM build, CDN/CSP support, -iframe storage, multi-tab coordination, offline cache updates, sustained frame -performance, or mobile/desktop packaging. The shipping Flutter example remains -native-only; production initialization and automatic asset delivery remain work -to implement after this research. diff --git a/research/mysql-precision-boundaries.md b/research/mysql-precision-boundaries.md deleted file mode 100644 index d02a47d9..00000000 --- a/research/mysql-precision-boundaries.md +++ /dev/null @@ -1,213 +0,0 @@ -# MySQL and MariaDB decimal precision boundaries - -Date: 2026-09-19. Tested against the local MySQL 8.4 and MariaDB 11.8 -integration containers. This report records actual SQL results, not a claim -about every release in either engine family. The probes used the database CLI, -independently of the Dart driver and SQL compiler. - -## Decision - -The MySQL and MariaDB `exactDecimal` capability means exact decimal value -transport and typed comparisons within supported storage limits. It does not -promise arbitrary-precision SQL arithmetic. The current typed SQL implementation -rejects decimal addition, subtraction, multiplication, `SUM`, explicit expression -`constrained`, and decimal `UNION` / `UNION ALL` with -`CAPABILITY.DECIMAL_PRECISION`. Explicit rounding, division, and average also -remain unsupported. The Dart `Decimal` value object's local arithmetic is -unaffected by these SQL restrictions. - -Decimal columns, exact parameters, comparisons, and `MIN` / `MAX` remain -available. The shared MySQL/MariaDB storage contract is precision 1–65 and scale -0–min(precision, 30); big integers use `DECIMAL(65,0)`. Assignments let the declared -column apply its engine's storage coercion and strict-mode checks. They do not -first cast the value into a narrower expression, which could saturate before -the column sees it. Storage coercion is not a promise of arbitrary precision or -of identical rounding across engines. - -Explicit `sql(...)` expressions remain the escape hatch when an application -accepts the selected engine's bounded arithmetic. Future typed support must -propagate concrete precision and scale through expressions and query boundaries, -and prove that the complete expression remains exact. We are not adding a SQL -big-number implementation or assuming that checking warnings makes these -operations safe. - -## Conditions and notation - -The probe sessions included `STRICT_ALL_TABLES` and `NO_BACKSLASH_ESCAPES`. -Each result-producing statement was immediately followed by `SHOW WARNINGS`. -The probes below only read values; they require no application tables. - -For readable result summaries: - -- `I = 10^65 - 1`, the maximum positive `DECIMAL(65,0)` value. -- `A = 10^35 - 10^-30`, the maximum positive `DECIMAL(65,30)` value. -- `E = 10^-30`. -- `F = 0.123456789012345678901234567891`. - -These symbols describe exact mathematical values; the SQL below uses explicit -literals and casts, not floating-point exponent notation. - -| Probe | MySQL 8.4 | MariaDB 11.8 | Warnings | -| --- | --- | --- | --- | -| `A + 1` | Exact, including the 36th integer digit | Same | None | -| `A + E` | Exact carry to `10^35` | Same | None | -| `I + E` | `I.000000000`; loses `E` | Same | None | -| `I - E` | `I.000000000`; loses the fractional subtraction and borrow | Same | None | -| `A - 1`, `A - E` | Exact | Same | None | -| `F + 1.2`, `F - 1.2` | Exact | Same | None | -| Direct `SUM` of two `A` values | Exact `2A` | Same | None | -| Direct `SUM` of two `I` values | Exact `2I` (66 integer digits) | Same | None | -| `SUM` of two `E` values | Exact `2E` | Same | None | -| The `2A` sum selected from a derived table or CTE | Saturates to `A` | Same | None | -| The `2I` sum selected from a derived table or CTE | Saturates to `I` | Same | None | -| Outer `SUM` over either saturated inner sum | Sees the already saturated value | Same | None | -| `F * F` | Result has 30 fractional digits; exact result needs 60 | Result has 38 fractional digits; exact result needs 60 | None | -| `CAST(999 AS DECIMAL(2,0))` | Returns `99` | Same | Warning 1264 | -| `UNION ALL` of `I` and `E` | Large row saturates to `A`; small row remains `E` | Same | None | - -Successful small examples do not establish a safe general operator. In -particular, a direct aggregate can appear exact while an ordinary surrounding -query changes the result. Only two input rows are needed to reproduce this. - -## Reproduction SQL - -Run the statements in a session with the modes above. `SHOW WARNINGS` must stay -immediately after its corresponding `SELECT`; later statements replace the -diagnostics. - -### Mixed-scale addition and subtraction - -Both operands fit the documented storage contract, but the result silently -discards the nonzero fractional term. - -```sql -SELECT - CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) - + CAST(0.000000000000000000000000000001 AS DECIMAL(65,30)) AS result; -SHOW WARNINGS; - -SELECT - CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) - - CAST(0.000000000000000000000000000001 AS DECIMAL(65,30)) AS result; -SHOW WARNINGS; -``` - -Both statements return the 65-digit integer followed by `.000000000`. Neither -engine reports a warning. The exact addition should preserve the final -fractional `1`; the exact subtraction should decrement the integer part and -produce 30 fractional nines. - -### Aggregate precision changes at a query boundary - -The first statement returns the exact 66-digit sum. The next statements return -only `I`, silently saturating the same sum. - -```sql -SELECT SUM(v) AS result -FROM ( - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) AS v - UNION ALL - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) -) AS inputs; -SHOW WARNINGS; - -SELECT s -FROM ( - SELECT SUM(v) AS s - FROM ( - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) AS v - UNION ALL - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) - ) AS inputs -) AS sums; -SHOW WARNINGS; - -WITH sums AS ( - SELECT SUM(v) AS s - FROM ( - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) AS v - UNION ALL - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) - ) AS inputs -) -SELECT s FROM sums; -SHOW WARNINGS; -``` - -The fractional case was also tested: replace each value with -`99999999999999999999999999999999999.999999999999999999999999999999` -and each cast with `DECIMAL(65,30)`. The direct result is -`199999999999999999999999999999999999.999999999999999999999999999998`. -The derived-table and CTE results instead equal the original single input `A`. -Changing the outer `SELECT s` to `SELECT SUM(s)` also observes the saturated -value. All these statements produce no warnings on either tested engine. - -### Multiplication - -```sql -SELECT - CAST(0.123456789012345678901234567891 AS DECIMAL(65,30)) - * CAST(0.123456789012345678901234567891 AS DECIMAL(65,30)) AS result; -SHOW WARNINGS; -``` - -The exact product requires 60 fractional digits. MySQL returns 30 and MariaDB -returns 38, with no warning. The different result scales also rule out treating -native multiplication as a single cross-engine exact contract. - -### Explicit narrowing cast - -```sql -SELECT CAST(CAST(999 AS DECIMAL(65,0)) AS DECIMAL(2,0)) AS result; -SHOW WARNINGS; -``` - -Both engines return `99` and warning 1264 rather than raising a statement error. -A preceding cast therefore cannot enforce an overflow-error contract on behalf -of a later column assignment. - -### Set-operation precision unification - -```sql -SELECT v -FROM ( - SELECT CAST(99999999999999999999999999999999999999999999999999999999999999999 - AS DECIMAL(65,0)) AS v - UNION ALL - SELECT CAST(0.000000000000000000000000000001 AS DECIMAL(65,30)) -) AS combined; -SHOW WARNINGS; -``` - -Both engines return `A` for the large row and `E` for the small row, without -warnings. The large input loses 30 integer digits merely by sharing a result -column with another valid decimal. The current typed guard covers both `UNION` -and `UNION ALL`; this concrete probe used `UNION ALL`. - -## Evidence and scope - -The SQL results above are the deciding evidence. They show that strict mode and -an empty warnings list are insufficient to establish exact expression results. -They do not imply that every native decimal operation is inaccurate, or that -all expressions with known small bounds must remain unsupported forever. - -MySQL's [decimal characteristics documentation](https://dev.mysql.com/doc/refman/8.4/en/precision-math-decimal-characteristics.html) -describes its bounded decimal representation. Its -[8.4 aggregate implementation](https://github.com/mysql/mysql-server/blob/8.4/sql/item_sum.cc) -contains separate aggregate result-type and temporary-field paths. Those are -useful implementation references; this report does not infer identical internals -for MariaDB or rely on an unverified explanation of the optimizer's decisions. - -The present policy deliberately rejects an operation whose complete bounds are -unknown instead of guessing from a value's Dart type. Restoring an operator -requires precision/scale propagation through parameters, columns, aggregates, -casts, subqueries, CTEs, and set operations, with regression coverage against -both real servers. diff --git a/research/new-dart-orm-design.md b/research/new-dart-orm-design.md deleted file mode 100644 index d15e78fc..00000000 --- a/research/new-dart-orm-design.md +++ /dev/null @@ -1,660 +0,0 @@ -# 全新 Dart ORM 设计研究 - -这份设计以低代码、类型安全、易用和功能完整为目标,从数据声明到数据库运行建立一套完整契约。研究范围包括关系型数据库的建模、查询、写入、关系加载、事务、迁移、驱动配置、生成工具和性能验证;PostgreSQL 与 SQLite 是首批验证对象,服务端、Flutter 原生端和 Web 分别讨论。事实核查截至 2026 年 9 月 14 日。 - -文中的 API 是设计提案,不是已发布或已实现的 ORM。标记为“实证”的内容通过独立 Dart 原型验证;数据库行为由对应官方文档支持;其余明确属于建议或待验证设计。现有项目实现不参与本设计的推导。 - -## 1. 推荐方向 - -建议采用 **Dart 数据类型声明字段、少量声明补充数据库语义、可组合的选择器决定结果形状、显式数据库会话执行操作** 的方案。 - -低代码入口优先验证带字段注解的 Record typedef。普通 class 可以作为需要名义类型、构造逻辑和实体方法时的另一种输入形式。两者应进入同一个生成流程与执行实现;首个可用版本先完成一种入口,避免同步维护四种建模语言。 - -推荐建立以下约定: - -| 事项 | 推荐方案 | 主要代价或限制 | -| --- | --- | --- | -| 字段声明 | Dart 类型声明一次;数据库规则通过注解或相邻约束声明补充 | 需要分析与代码生成 | -| 表身份 | 每张表拥有独立生成描述符;记录值不承担表身份 | Record 别名本身不是名义类型 | -| 关系声明 | 基于键的显式关系;反向关系复用同一条边 | 自关联、多外键必须有清晰名称 | -| 字段选择 | `Selection` 同时持有字段计划和解码方式 | 任意命名结果需要一次显式映射 | -| 关系结果 | `Selection`、`Selection>` 与普通字段组合 | 批量加载和数据库内聚合需要真实执行器验证 | -| 简单写入 | 生成直接命名参数的快捷方法 | 有默认值与显式 null 的字段需要区别表达 | -| 复杂写入 | 可组合表达式、返回字段选择、显式事务 | 不把任意对象图自动变成写入命令 | -| 事务 | `transaction((tx) async { ... })` 绑定连接 | Dart 无法静态禁止 tx 逃逸,需运行时生命周期检查 | -| 迁移 | 可审阅的版本化迁移;schema 是目标,数据库探测是实况 | 重命名和数据回填不能完全自动推断 | -| 驱动配置 | 每类驱动有自己的强类型配置;应用数据库入口统一 | 相同 SQL 方言不保证相同事务或流式能力 | - -这套方案的新价值在于把简短声明、准确结果类型和可解释执行成本连起来。Record、注解、选择器和关系描述本身都有既有技术基础;不宣称这些单项概念是首次出现。 - -## 2. 最新 Dart 能力与实际边界 - -本机直接调用独立 SDK 得到 `Dart SDK version: 3.13.3 (stable)`。官方 Dart 3.13 于 2026 年 8 月发布,主构造函数已稳定。[^1] - -| 能力 | 对 ORM 的实际作用 | 不能据此承诺的事情 | -| --- | --- | --- | -| 主构造函数,3.13 | 缩短普通模型类的字段和构造器声明 | 自动生成数据库映射、值相等或 copyWith | -| Dot shorthand,3.10 | 上下文明确时使用 `.new()`、`.set()`、`.desc` | 任意位置省略类型名,或增加新 DSL 语法 | -| Records,3.0 | 异构结果、命名投影、结构相等 | 通用 mapped types、任意字段枚举、把 Expr 字段自动拆成值 | -| Patterns、sealed classes | 解构结果、穷尽处理状态和错误 | 在编译期证明 SQL 业务约束 | -| Extension types | 低额外包装成本的 UserId、编码值等接口 | 运行时身份隔离、动态反射识别领域类型 | -| 泛型扩展方法 | 对有限 arity 的异构字段组合做类型推断 | 对任意 Record 形状逐字段递归变换类型 | -| build_runner/watch | 常规生成、增量构建、生态集成 | 无生成步骤或生成成本为零 | - -Dot shorthand 与 extension types 的能力依据官方语言说明。[^2][^3][^4] Macros 的原有工作已停止,当前 build_runner 文档仍将生成作为编译器不支持 macros 时的可用路径。Flutter 不提供 mirrors 反射系统。原型在稳定 SDK 上也未通过 `augment class`;本设计不依赖未稳定的增强功能。[^5][^6][^7] - -**实证:Record 字段允许元数据。** 下列声明可以通过 Dart 3.13.3 分析: - -```dart -typedef User = ({ - @Id.generated() int id, - @Unique() String email, - String? nickname, -}); -``` - -`Id` 与 `Unique` 是提案中的普通 const 注解。元数据需要生成器读取源码;运行时 Record 对象不会携带可供 ORM 枚举的字段注解。 - -**实证:Record 别名是结构类型。** 若 User 与另一种别名拥有完全相同的字段形状,两者可以相互赋值。`User(id: ...)` 也不是 Record 的构造方式。因此默认写入 API 应围绕生成的 `db.users.create(...)`,表身份使用独立描述符;不能使用 `Map` 区分同形的 Record 实体。[^3] - -**语言边界:不能用普通布尔闭包冒充 SQL 表达式树。** `&&`、`||`、`!` 不可重载,`==` 返回 bool。因此运行时查询条件使用 `.eq()`、`.and()`、`.or()` 等表达式操作。若未来提供 `(u) => u.age > 18 && u.active`,它必须是一种独立、受限、明确生成的查询声明语法。[^8] - -## 3. 用完整使用过程衡量设计 - -低代码衡量“修改一个字段需要改几处”“写一段真实业务需要多少概念”,不能只比较模型行数。类型安全需要同时覆盖输入、表达式、查询结果、空值、关系形状和解码边界。功能完整则需要覆盖数据从创建到长期升级的整个过程。 - -| 验收场景 | 应得到的体验 | -| --- | --- | -| 新增一个有默认值的字段 | 改声明、生成、审阅迁移;已有创建调用仍可省略该字段 | -| 查询用户的邮箱列表 | 实际 SQL 只选邮箱;返回 `List` | -| 用户+每人最近三篇文章标题 | 根分页正确,子限制按每个父记录计算,无逐行查询 | -| 创建用户及文章 | 可在同一事务取得生成 ID,再创建子记录 | -| 清空昵称、增加浏览量 | 显式 null 与未修改区分;递增在数据库完成 | -| 变更字段名称 | 代码重命名与数据库列重命名分别处理 | -| 切换 SQLite 文件/内存/Web | 主要改变连接配置,无法支持的能力明确报错 | -| 替换 PostgreSQL 驱动 | 保留模型和查询 API;重新验证事务、取消、编码能力 | -| 处理大结果集 | 分页或真实流式读取,释放会话可验证 | -| 部署新版本 | 应用所需 schema 版本、迁移状态和实际数据库差异可检查 | - -## 4. 声明模式:超过“类还是 DSL”的比较 - -| 声明方式 | 优点 | 长期代价 | 建议角色 | -| --- | --- | --- | --- | -| 普通 class+注解 | Dart 熟悉、名义类型、可写方法和构造逻辑 | ORM 要约束可映射构造方式;实体对象容易混入加载状态 | 需要领域对象时使用 | -| Record typedef+注解 | 最少数据样板;字段类型直接是 Dart 类型;自然解构和值相等 | 无实体名义身份;没有普通类构造器与实例方法 | 优先验证的低代码入口 | -| typed Dart schema DSL | 数据库类型、约束表达集中,列对象能直接参与关系配置 | 需要学习字段构造方法;普通行类型仍需生成 | 数据库优先项目的备选,不与默认入口同步建设 | -| class 型表声明 | 每个列成员有符号身份,复合约束和跨表引用方便 | getter/column 类型样板较多,常需另外生成行类 | 对照方案,不因既有生态成熟就默认采用 | -| SQL 文件/数据库优先 | SQL 能力完整;已有数据库接入自然 | Dart 类型要生成;跨数据库声明需要分别维护 | 已有数据库导入、命名复杂查询与迁移的正式路径 | - -推荐的第三种组织方式是:**数据类型声明基础字段,相邻的强类型声明描述键、关系和索引。** 这样既不要求把所有数据库配置塞进构造参数,也不要求重新用列 DSL 复写每个字段的 Dart 类型。 - -### 4.1 候选声明 - -```dart -typedef User = ({ - @Id.generated() int id, - @Unique() String email, - String? nickname, -}); - -typedef Post = ({ - @Id.generated() int id, - int authorId, - String title, - DateTime createdAt, -}); - -final users = entity(); -final posts = entity(); - -final author = posts - .key((p) => p.authorId) - .references( - users.key((u) => u.id), - inverse: 'posts', - onDelete: .restrict, - ); - -final authorTimeline = posts.index( - (p) => (p.authorId, p.createdAt, p.id), -); -``` - -上例是**生成器声明 API 候选**,并非已验证的整套生成实现。`entity()` 绑定声明中的 User 与 users 表身份;默认数据库名称从声明推导,支持显式 table/column 映射。`author` 声明提供 Post 到 User 的关系名称,`inverse` 一次性命名反向关系。这个字符串是新 API 名称的声明,不是查询时使用的字符串字段引用。 - -`.key((p) => p.authorId)` 的闭包只承担源码声明。生成器只接受字段访问和字段 Record 组合,拒绝任意业务计算;不能在运行时执行一个虚构 Post 来发现列。字段拼写和 Dart 类型可以由 analyzer 检查,目标是否为唯一键、列的数据库类型是否匹配、路径是否属于受限语法,由生成器检查。此处需要一个小型明确的语法读取器,不能扩张成通用 Dart 解释器。 - -这一方案无需先生成 `UserFields` 再让 schema 引用它,可以避免“声明依赖尚未生成代码”的启动循环。是否足够简短、编辑器重命名体验是否可靠,仍需做真实 analyzer/generator 原型。 - -### 4.2 复杂关系和约束 - -一对一需要外键加唯一约束;仅命名成 `one` 不足以保证唯一。多对多使用显式中间表,可包含加入时间、角色等业务字段。复合主键和外键以同序字段 Record 配对声明,不强制虚构单列 id。自关联和同表间多个外键各有独立关系名。 - -```dart -final membershipKey = memberships.primaryKey( - (m) => (m.teamId, m.userId), -); - -final accountOwner = accounts - .key((a) => (a.tenantId, a.ownerId)) - .references(users.key((u) => (u.tenantId, u.id))); -``` - -这是扩展示意,不与前面的单列 User schema 同时使用。复合键要求列数、次序、底层存储类型及目标唯一性一致。索引、唯一约束、外键各有不同目的,不能自动给所有外键额外叠加重复索引。 - -关系导航和数据库约束关联但不等价。生成器默认让正式外键生成导航边;也允许显式声明无数据库外键的只读关系,但必须标记其较弱的完整性保证。Drizzle 的关系文档展示了关系与外键分离的价值;当前所查 `defineRelations` 页面使用 RC 安装示例,只作为设计参考。[^9] - -### 4.3 默认值与名称 - -Dart 构造默认值、客户端生成值、数据库 DEFAULT、数据库 generated column 是四件不同的事。元数据必须分别表达;不能把 Dart 的 `DateTime.now()` 偷换成数据库时间,也不能仅根据一个普通类的默认参数自动修改数据库 DEFAULT。 - -字段重命名优先保留明确列映射。需要真的重命名数据库列时,写入迁移操作。生成代码、schema 目标定义、历史迁移之间可以有派生信息重复;禁止的是多份需要人手同时维护的事实。 - -## 5. 类型契约 - -建议至少区分行值、创建输入、更新指令、SQL 表达式和查询结果。它们由同一声明派生,不需要开发者手写五套实体类。 - -| 类型或规则 | 编译阶段能保证什么 | 仍需其他阶段保证什么 | -| --- | --- | --- | -| `Expr` 与字段操作 | 参数基础类型,支持的操作类别 | 数值范围、排序规则、SQL 三值逻辑细节 | -| `Selection` | 返回 Record/DTO 的具体字段类型 | 数据库实际返回值与 codec 合法性 | -| 生成创建参数 | 必填字段,禁止写计算列 | 唯一冲突、外键存在性、CHECK | -| `Change` | 非空字段不能 `.set(null)` | DEFAULT 是否存在、目标列可否更新 | -| 生成表身份 | 查询与写入绑定哪张表 | alias 是否属于当前 SQL 作用域 | -| 后端泛型 | PostgreSQL 专用 API 不出现在 SQLite 实例上 | 实际服务器版本及连接驱动能力 | -| 显式 tx 对象 | 调用代码可以传递事务会话 | 事务结束后误用、跨连接或未等待操作 | - -NULL 使用 `.isNull()`、`.isNotNull()` 明确表达;非空字段 `.eq()` 只接受非空 T。空 `IN` 集合应有确定的 false 语义。存在 null 的集合不能依赖 Dart 集合语义猜测 SQL 的 `IN`/`NOT IN` 结果。可空聚合、外连接、过滤后的关系也必须反映在输出类型中。 - -数据库类型映射需要独立元数据与 codec: - -| 领域 | 推荐处理 | -| --- | --- | -| 整数 | 指定数据库位宽;跨 JS 编译边界的完整 64 位值使用验证过的 BigInt/Int64 codec | -| 精确十进制 | 使用 Decimal codec 或可验证的精确表示,不能默认转 double | -| 时间点 | 明确 UTC instant 存储;区分无时区 timestamp、date、time 和时区名称 | -| JSON | JSON 值与任意 Dart 对象分开;显式 codec 负责验证 | -| 枚举 | 默认稳定文本值;原生数据库枚举是后端能力和迁移选择 | -| 二进制 | 统一 Uint8List 等明确边界,避免每层复制 | -| 自定义 ID | 可用 extension type;编解码器必须注册对应存储表示 | - -Dart JS 数值范围与原生整数范围不同;PostgreSQL numeric 是精确数值类型;SQLite 的类型亲和性与 STRICT 表规则也不同。这些差异不能用一个 `String/int/double` 映射表完全掩盖。[^10][^11][^12][^13] - -## 6. 字段选择与复杂查询 - -### 6.1 一套可实现的选择协议 - -```dart -final emails = await db.users - .select((u) => u.email) - .get(); // List - -final cards = await db.users - .select((u) => (u.id, u.email).map( - (id, email) => (id: id, email: email), - )) - .get(); // List<({int id, String email})> - -final objects = await db.users - .select((u) => (u.id, u.email).map(UserCard.new)) - .get(); // List -``` - -**实证:** 字段组合到命名 Record、DTO 构造器、单字段、可空整个对象及异构嵌套列表的泛型推断已通过 Dart 3.13.3。规划阶段不调用映射函数;只有行值到达时才解码并调用函数。 - -最小内部关系是 `Expr implements Selection`,组合后的选择器仍是 `Selection`。它需要知道列依赖及如何重建 R。示例原型使用字符串标签和合成行数据;真实实现应使用列引用、作用域和位置解码,不应照搬原型中的字符串 Map。 - -对 2—6 项提供少量通用扩展可降低使用成本。更大结果通过嵌套组合或显式命名投影完成,不预生成所有字段组合。具体快捷 arity 数量是工具体积与使用频率的测量结果,不是功能上限。 - -### 6.2 明确放弃的伪简化 - -```dart -// 不作为普通运行时库的承诺: -select((u) => (id: u.id, email: u.email)); - -// 只能安全解释为解码回调,不能独自确定 SELECT 列: -select((u, read) => (id: read(u.id), email: read(u.email))); -``` - -第一种回调实际返回 Expr 的 Record,Dart 不会通用地把每个字段解包成值。第二种写法能推断值类型,但尝试以假数据执行来发现列,会被条件分支、自定义 codec 和副作用破坏。原型中 `read(id) > 0 ? read(email) : 'hidden'` 就证明了假数据发现的列可能不完整。 - -命名的生成式 query/view 可以未来支持更简短声明:构建时读取固定查询,生成参数和结果类型。它应是显式能力,不应扫描并转换应用里的所有任意闭包。Drift 的 SQL 自定义查询展示了生成类型化查询的实际路径。[^14] - -### 6.3 动态查询与 SQL 上限 - -`where/orderBy/limit` 返回可复用的查询值,条件可以通过普通 Dart if 组合;只有终结操作执行。`.get()` 返回列表,`.first()` 返回可空单条,`.single()` 校验恰好一条,`.count()` 和 `.exists()` 使用专门 SQL。筛选常量全部绑定参数,标识符来自 schema 或显式允许的动态字段集合。 - -join、group by、having、聚合、子查询、CTE、窗口函数、union 应使用同一表达式与选择协议。聚合合法性、表别名归属等无法经济地完全塞进 Dart 类型参数的问题,在 SQL 发出前验证。原生 SQL 是正式出口:参数绑定与显式 decoder 是基础保证;只有经过解析或数据库检查的命名 SQL 才能声称查询结构也经过静态验证。 - -CTE 必须保留 SQL 列身份:`.map()` 构造出的 Record/DTO 属性不是 SQL 列,不能自动提供 `cte.someProperty`。候选使用 `cte.ref(selectedExpression)` 得到准确的 `Expr`,是否确实导出了该表达式由规划阶段验证。需要静态命名属性的 CTE/view 则通过明确的命名查询生成类型。mapper 中任意 Dart 计算不能反向成为数据库表达式。 - -动态勾选任意字段的后台导出无法同时得到每种运行时组合的静态 Record 类型。提供显式动态结果 `Map`,或一组预定义投影;不要把强制转换包装成类型安全。 - -分页同时提供 offset 与复合 keyset。keyset 必须明确稳定排序、唯一 tie-breaker、null 顺序和游标版本;不得对未定义顺序的结果宣传稳定翻页。 - -## 7. 关系数据作为可组合查询结果 - -关系描述存储连接规则;关系查询存储筛选、排序、投影和加载策略;行值只存已查询的数据。这样一个 `User` 不必永久携带 `List?` 或隐式懒加载代理。 - -```dart -final rows = await db.users - .orderBy((u) => [u.id.asc()]) - .take(20) - .select((u) => ( - u.id, - u.email, - u.posts - .orderBy((p) => [p.createdAt.desc(), p.id.desc()]) - .take(3) - .select((p) => p.title) - .many(), - ).map((id, email, titles) => ( - id: id, - email: email, - titles: titles, - ))) - .get(); -``` - -候选结果是 `List<({int id, String email, List titles})>`。没有选择的字段在类型中不存在;已经加载但没有子行的列表是空列表。可空单关联用 `T?`;外连接整行可空依赖明确的存在性标记,不能把每个字段变 nullable 再假装一定有对象。 - -上例的泛型组合已经通过合成数据原型;关系 SQL 编译、分组装配、分页正确性尚未实现。一个选择器可以表达多个 SQL 步骤,不要求所有 ORM 调用强制单 SQL。 - -### 7.1 加载策略与成本 - -| 策略 | 推荐用途 | 必须处理的代价 | -| --- | --- | --- | -| 同语句 join | 单关联、显式扁平 join | 多个一对多会放大行数;根 limit 不能作用到展开后的行 | -| 批量 select-in | 默认的一对多/多对多加载 | 多次往返、参数数量限制、跨语句一致性 | -| 数据库聚合 | 支持目标上的 JSON/array 嵌套结果 | 数据库 CPU、序列化、null/时间/精确数值 codec | -| 显式懒查询 | 单个对象详情或刻意延后加载 | 循环里调用容易形成 N+1,必须能看见执行次数 | - -默认策略应确定且可检查:简单行与一对一/多对一尽量一条语句;一对多/多对多列表使用批量加载;需要特殊策略时允许在关系边上指定。不要加入未经测量的自适应“智能优化器”。SQLAlchemy、Drizzle 和 Kysely 的相关能力是评估对照,不是默认必须照搬的规则。[^9][^15][^16] - -查询数按关系层级与分块增长,不按每个父对象增长。例如没有子关系且无分块时,一组根行加一条列表关系通常是两次查询;实际次数还取决于复合键宽度、参数上限和嵌套层数。根结果为空时不查询子关系。 - -“每人三篇文章”必须使用分区窗口、相关子查询或等价的按父级限制方案。不能对 `WHERE author_id IN (...)` 直接加全局 `LIMIT 3`,也不能拉取所有文章后在 Dart 丢弃绝大部分数据。 - -多语句加载需要明确一致性:一般读取可以接受各语句各自快照;需要统一视图时选择明确的快照事务。在 PostgreSQL 下,仅套一个默认 Read Committed 事务不等于所有语句使用同一快照。[^17] - -### 7.2 关系筛选与边界 - -提供 `any`、`none`、`every`、`count` 与按关系字段排序。通常降低成 EXISTS、NOT EXISTS 或聚合子查询。`every` 对空集合为真必须文档化,必要时与 `any` 联合使用。建议把谓词 UNKNOWN 当作未满足;编译为不存在“谓词不为 TRUE”的子行,并测试可空字段,不能简单把 `NOT predicate` 等同于这条规则。不为了关系过滤自动读取整个关联对象。 - -一条非空外键并不保证经过权限过滤、软删除或自定义过滤后仍能看见目标。可见性受筛选影响的关系需要可空结果,或由显式的 required 查询在缺失时报告错误。多态关系、无约束关系和跨数据库关系不享有普通外键的完整性保证。 - -## 8. 写入与默认值 - -日常创建用生成的命名参数快捷入口: - -```dart -final user = await db.users.create(email: 'a@example.com'); - -await db.users.byId(user.id).update( - nickname: .set(null), -); -``` - -需要选择返回列、批量写入或表达式时使用相同执行实现的 builder: - -```dart -final id = await db.users - .insert(.new(email: 'a@example.com')) - .returning((u) => u.id) - .single(); - -await db.posts.byId(postId).updateWith((p) => [ - p.views.increment(1), - p.title.set('New title'), -]); -``` - -`views` 是后续扩展字段示意。这里 `increment` 返回写入指令,与查询表达式的加法区别明确。简单方法只是预设操作,不应拥有另一套 SQL 引擎。 - -更新内部使用三态或操作集合:省略、设值(可能为 null)、数据库 DEFAULT。非空字段不得接受 `.set(null)`。`.defaultValue()` 除要求该列有 DEFAULT,还要验证后端支持;SQLite 没有 PostgreSQL 式的 `UPDATE SET column = DEFAULT`,不能直接生成这条语句。若提供默认表达式展开,只支持已经定义且可保持语义的情况,否则明确拒绝。计算列和只读列不进入普通写入参数。空更新应在执行前拒绝,返回明确错误。[^48] - -创建时,非空且无默认值的字段必填;数据库生成字段可省略;可空字段如果还具有数据库默认值,则必须能区分“使用默认值”和“显式 null”。快捷创建方法不应靠 `Object?` 加运行时强转把所有输入类型放宽。 - -批量插入按参数限制分块,默认返回影响行数,只有显式 returning 才读取结果。一次批量调用默认让所有分块位于同一事务,已有 tx 时复用其范围;超大导入逐块提交必须显式选择,并返回可恢复的进度和部分失败结果。数据库未规定的返回顺序不能被视为输入顺序;依赖输入对应关系的场景使用稳定键或显式关联标识。SQLite returning 还不保证包含随后 AFTER trigger 的修改,不得声称返回的是所有触发器完成后的最终行。[^49] - -upsert 基于唯一约束执行,不能用先查再插入模拟原子性;冲突目标、更新表达式、返回结果均需保留数据库差异。原子递增与乐观并发使用 SQL 表达式和版本条件实现,更新零行与唯一冲突是不同结果。 - -关联写入首先用显式事务表达 create/connect/disconnect/delete 的顺序,之后再增加具有清晰语义的嵌套语法糖。不对传入的任意 User/Post 对象图做脏检查和自动级联保存。这样可以避免隐藏查询、意外删除与复杂的对象身份管理。 - -## 9. 事务、并发与生命周期 - -```dart -final user = await db.transaction((tx) async { - final user = await tx.users.create(email: 'a@example.com'); - await tx.posts.create( - authorId: user.id, - title: 'Hello', - createdAt: DateTime.now().toUtc(), - ); - return user; -}); -``` - -tx 暴露同样的表 API,并绑定一条明确的连接。callback 正常完成后提交,提交成功才完成外部 Future;异常时回滚。tx 在作用域结束后失效。复用业务方法可以传入执行会话,而不是依赖全局单例或 Zone 自动把任意 db 调用转成事务调用。 - -所有异步查询必须 await。Dart 类型系统无法证明没有未等待 Future,也无法禁止 tx 被保存到外部对象;运行时必须拒绝失效会话,并检测 callback 结束时仍在进行的工作。不能自动提交一个仍有未完成语句的事务。Drift 的事务说明也明确要求等待事务内的异步操作。[^18] - -### 9.1 嵌套与隔离 - -```dart -await db.transaction((tx) async { - await tx.users.create(email: 'a@example.com'); - try { - await tx.savepoint((sp) async { - await sp.invites.create(code: inviteCode); - }); - } on UniqueViolation { - // 仅撤销内层,外层继续。 - } -}); -``` - -内层成功只是释放 savepoint,最终持久化仍依赖外层提交。内层失败后,只有确认外层事务仍活跃且 rollback-to/release 成功,才能允许外层继续;SQLite 的部分严重错误可能已经整体回滚,连接故障也不能靠 savepoint 恢复。无法恢复时使外层失效并传播错误;异常未被捕获时外层也结束。同连接 savepoint 必须保持栈顺序,不允许并发异步块交错操作,也不为每条 SQL 自动增加 savepoint 往返。[^19][^20][^22] - -| 设置 | PostgreSQL | SQLite | -| --- | --- | --- | -| 默认事务 | Read Committed | Deferred | -| 稳定读快照 | Repeatable Read 等明确语义 | WAL 模式下的读事务快照 | -| 写并发 | MVCC 与锁协调多个写事务 | 同一数据库同一时间只有一个写事务 | -| 提前开始写事务 | 数据库锁与事务选项 | Immediate | -| 后端专有设置 | isolation、readOnly、deferrable | deferred、immediate、exclusive | - -不把 PostgreSQL 隔离级别名称机械映射到 SQLite。对于动态选择的驱动,不支持的设置在执行前报错;绝不静默降级。SQLite COMMIT 遇到 BUSY 时,事务可能仍活跃;需要区分重试提交和重跑整个 callback,不能一律回到事务开头。[^17][^21][^22] - -### 9.2 重试与提交未知 - -自动重试默认关闭。显式启用时,只对适配器已正确分类的可重试错误重跑整个事务 callback,并限制次数与总时间;PostgreSQL 序列化失败要求连同决定 SQL 的应用逻辑一起重试,不能只重发最后一条 SQL。[^23] - -commit 时连接断开,可能无法确定数据库究竟提交还是回滚。此时返回 `CommitOutcomeUnknown` 一类明确结果,不无条件重跑。外部邮件、支付或事件等副作用不能因 callback 重试而被假定只执行一次。afterCommit 只表示当前进程观察到提交后执行;可靠事件投递可使用同事务 outbox 配方,ORM 不必因此内建消息系统。 - -### 9.3 超时与取消 - -需要分别建模等待连接、连接建立、语句运行、等待数据库锁、事务整体期限。`Future.timeout` 不会取消原始 Future,因此不足以证明 SQL 已停止。SQLite busy timeout 只处理锁等待,也不等价于语句运行上限。[^24][^25] - -取消流程应停止接收新操作、发送真实取消或触发数据库期限、处理游标和协议剩余响应、完成回滚,再确认连接可复用。无法确认状态就销毁该连接,不能直接归还连接池。SQLite 的真实中断依赖驱动提供相应能力。[^26] - -## 10. 迁移是产品主能力 - -当前 schema 表达目标;版本化迁移表达如何到达目标;数据库 introspection 表达实际状态。这三者各有职责,不能用启动时自动 diff 代替部署历史,也不能只检查一个版本号就声称数据库没有漂移。 - -### 10.1 工作流 - -候选命令保持生成、审阅、执行清晰: - -```text -dart run orm generate -dart run orm migration create add_nickname -dart run orm migration check -dart run orm migrate --plan -dart run orm migrate -dart run orm db inspect -dart run orm db verify -``` - -这些是提议的命令名。默认先用线性历史,迁移文件包含顺序 ID、目标数据库、SQL、校验和及事务方式。迁移记录保存执行状态和失败阶段。schema snapshot 由工具维护,用于差异生成和旧版本测试。已部署迁移不允许随意改写;只提交当前 schema 不足以保留手写数据变换的历史。[^27] - -一套应用迁移历史固定一个数据库引擎,包括空注册表。单个迁移只有该引擎的一组步骤、转换 SQL 和历史物理 schema;ORM 的多库编译能力不进入应用的迁移文件。共享声明的另一引擎配置不影响当前历史的 diff 或指纹。多库应用分别维护目录、注册表和连接;换库属于数据搬迁及新库 baseline,不能重放或改写原库历史。生产运行器只执行已审阅迁移。Flutter 本地数据库可以在业务开始前应用打包的迁移,服务端则建议作为独立发布步骤。 - -### 10.2 改名、回填和兼容升级 - -diff 无法确定“删除 name、新增 displayName”到底是重命名还是两个独立动作。生成器可以提出候选,但需要明确 rename 记录;CI 遇到歧义不得自行选择删除数据。保留数据库列名的 Dart 重命名不应该产生数据库 DDL。 - -大数据变换按 expand → backfill → contract 进行:先增加兼容结构,再发布兼容代码,分块回填并校验,切换使用,最后确认旧进程和作业退出后删除旧结构。长回填有进度、checkpoint、重复运行条件和完成证明。回填逻辑使用该迁移的历史 schema,不导入永远变化的最新应用模型。Drift 的版本化迁移与测试工具提供了可参考的实际做法。[^28][^29] - -没有通用的自动无损回滚。类型转换、拆列和删除字段可能不可逆;默认前向修复,显式 down 或恢复备份由具体迁移定义语义。应用降级遇到更高 schema 版本时应报不兼容,不自动删表重建。 - -### 10.3 数据库差异与失败恢复 - -PostgreSQL `CREATE INDEX CONCURRENTLY` 不能运行在事务块内;失败还可能留下 INVALID 索引。迁移必须能表达非事务步骤,并通过前置/后置检查处理“SQL 成功但进程未记账就退出”的情况。`IF NOT EXISTS` 不能证明同名对象定义正确。[^30] - -事务性迁移的 DDL 与成功记录应在同一事务提交。包含非事务步骤的迁移使用专用连接与合适的运行器锁,失败后先检查实况,再决定恢复。PostgreSQL 的会话级与事务级 advisory lock 生命周期不同,使用方式要匹配迁移范围;锁只协调遵循该协议的运行器,不能阻止手动 ALTER。[^31] - -SQLite 的部分结构变更需要新建表、显式列映射复制、替换表、重建受影响索引/触发器/视图。必须按正确顺序处理外键设置并运行 foreign_key_check;不能把所有 ALTER 简化成一个改字符串操作。未知依赖对象必须保留或明确交给手写迁移。[^32][^33] - -### 10.4 已有数据库、baseline 与 drift - -introspection 覆盖受管表、列、约束、索引及可理解的附属对象,输出支持范围和未建模对象。已有数据库可导入声明初稿并验证 baseline;登记 baseline 不应重新执行建表或搬动已有数据。[^34] - -完整 drift 检查比较实际结构与迁移预期。默认函数、表达式索引、自动生成名称等由数据库专有规范化处理。无法理解的 trigger、view、RLS policy、extension 不等于应删除的对象。 - -成本上,应用常规启动做轻量版本兼容检查;明确的 verify/CI/迁移流程做适当范围的真实结构检查。每次打开数据库都全量扫描 catalog 并无必要。迁移预览应展示重建、扫描、锁与非事务步骤的已知影响,不宣传未经验证的“零停机”。 - -## 11. 不同数据库与驱动配置 - -数据库方言、传输驱动、部署环境是不同因素。PostgreSQL 的 TCP 驱动与 HTTP 驱动可能支持相似 SQL,却拥有不同的连接、交互事务、取消和流式协议。SQLite 原生 FFI 与浏览器 WASM 也不能使用完全相同的文件和线程配置。 - -**实证:** `AppDatabase` 可以从 `Driver` 推断后端类型;每种驱动接收不同的配置类型。原型正确拒绝把 SqliteOptions 传给 PgDriver,也拒绝在 `AppDatabase` 上调用 PostgreSQL 专用扩展。 - -### 11.1 日常配置入口 - -以下是产品 API 候选;底层参数名称由适配器映射,并非对应驱动现成类的逐字用法。 - -```dart -final db = await AppDatabase.open( - PostgresDriver(.new( - url: config.databaseUrl, - tls: .verifyFull, - pool: .new( - maxConnections: 8, - acquireTimeout: Duration(seconds: 5), - ), - )), -); -``` - -```dart -final db = await AppDatabase.open( - SqliteDriver.file( - 'app.sqlite', - execution: .background, - journal: .wal, - busyTimeout: Duration(seconds: 5), - ), -); - -final testDb = await AppDatabase.open(SqliteDriver.memory()); -``` - -```dart -final db = await AppDatabase.open( - SqliteWebDriver( - name: 'app', - wasm: Uri.parse('/sqlite3.wasm'), - worker: Uri.parse('/database_worker.js'), - storage: .opfs, - ), -); -``` - -示例中的连接数与超时是配置演示,不是通用性能默认值。简单用户选一个驱动并打开生成的数据库入口即可。默认行为应保守、确定、有文档;对性能和部署敏感的选项允许精确调整。 - -### 11.2 配置职责 - -| 类别 | 典型配置 | 应归属的位置 | -| --- | --- | --- | -| 端点和认证 | URL、socket、数据库、用户名、证书 | 对应网络驱动 | -| 连接池 | 最大连接、借用等待、寿命、回收 | 池提供者;不再在外面套第二个池 | -| 会话 | 时区、search_path、编码、初始化 | 驱动/后端,明确何时设置与清理 | -| SQLite 文件 | 路径、只读、journal、busy timeout | SQLite native 驱动 | -| SQLite 执行线程 | 本 isolate、后台 isolate | native 执行配置 | -| Web 存储 | WASM、worker、OPFS、IndexedDB | Web 驱动 | -| 查询期限 | acquire/connect/statement/transaction | 能准确实现该期限的一层 | -| 类型映射 | 原生数据库类型、应用 codec | ORM schema 与驱动 codec 桥接 | -| 迁移 | 发布时运行/打开前升级/仅验证 | 应用初始化与迁移工具 | -| 日志 | SQL 模板、参数脱敏、耗时、trace | ORM 执行观察接口 | - -配置由调用者显式传入。环境变量、文件、配置服务可以在应用层解析成这些类型;ORM 不依赖某个特定配置框架。连接字符串和显式参数冲突必须拒绝或有唯一明确的优先规则;不得静默忽略未识别选项。日志不输出连接密码、token 或默认展开所有绑定值。 - -### 11.3 现有驱动与适配范围 - -| 后端与运行方式 | 当前依据 | 推荐策略 | -| --- | --- | --- | -| PostgreSQL native | `postgres` 3.5.12 提供二进制协议、连接池、事务会话和 statement 能力 | 优先适配,复用其连接池与协议实现 | -| SQLite native | `sqlite3` 3.6.0 提供底层连接、prepare、执行、更新通知等 | 复用引擎绑定;Flutter 默认验证后台 isolate 路径 | -| SQLite Web | sqlite3 WASM+worker+浏览器文件系统 | 独立平台配置,查询和选择协议共用 | -| 其他 PostgreSQL 驱动 | 如 `pg` 有不同实现与发布者基准 | 作为可替换适配器,先用同一工作负载验证 | -| MySQL/MariaDB | Dart 存在带 TLS、池与事务的驱动候选 | 下一批适配;先核实认证、类型、DDL 与返回值契约 | -| HTTP/serverless SQL | Turso、Neon 展示 HTTP/WebSocket 的不同连接方式 | 按传输与会话能力建模,不冒充 native 长连接 | - -版本号来自研究时的包页;未进行驱动性能排名。`postgres` 的连接设置和池设置已提供大量本来就属于驱动的参数,没必要全部重新发明。[^35][^36][^37][^38] `pg` 发布者的性能数据没有在本研究复现,因此不据此更换首选驱动。[^39] - -PostgreSQL TLS 不能只用 `ssl: true` 表示安全级别:`postgres` 的 require 与 verifyFull 行为不同,verifyFull 才验证证书。推荐网络部署默认采用可验证的 TLS 配置,兼容本地 socket 或明确指定的开发配置。[^40] - -SQLite FFI 执行是同步的,主 isolate 大查询可能影响 UI;后台 isolate 应长期持有连接,不为每个查询重复启动。内存库的生命周期通常随连接,不能创建多个私有 `:memory:` 连接后假设它们共享数据。WAL 也不会把 SQLite 变成多写者数据库。[^41][^42][^43] - -SQLite 正常会话应在事务外开启并验证每条连接的 `PRAGMA foreign_keys = ON`,覆盖 native、Web 和借入连接。若借入的事务连接尚未启用外键,不能在事务中假装设置成功;应拒绝提供相应完整性保证或要求调用方先正确初始化。迁移临时调整设置后也必须恢复和复验。[^33] - -Web 配置必须验证 WASM/worker 版本兼容、持久化方式、跨标签页协调和必要的浏览器能力。若所选持久化不可用,应返回明确结果或采用调用者允许的 fallback,不能悄悄退化成会丢数据的内存库。[^44] - -MySQL/MariaDB 当前只确认存在候选 Dart 驱动,并未完成兼容认证。SQL Server 等也不因有通用 Driver 接口就自动获得支持。Turso 与 Neon 的官方示例验证了传输形态差异,但其中的 JS SDK 不能直接当 Dart 依赖;对应 Dart 适配器仍需实现和测试。[^45][^46][^47] - -### 11.4 驱动替换与所有权 - -支持两条清楚的路径:ORM 创建并拥有驱动资源,或调用者提供现有连接池/会话。后者需要明确 `borrow` 语义,`db.close()` 不擅自关闭外部仍在共享的连接池;也可以显式移交所有权。 - -驱动接口保持直接:获取会话、执行参数化语句、启动和结束事务、释放资源。prepared statements、取消、游标、批量、更新通知作为具体能力扩展,避免要求每个驱动实现几十个无意义空方法。 - -能力来自方言、实际服务器版本、编译选项、驱动和传输的交集。一个布尔 `supportsSqlite` 或 URL 前缀不足以决定 returning、交互事务、参数上限、流式或取消。已知后端能力可以静态暴露;连接时才能确认的能力在规划前验证。 - -多个数据库通过多个明确的实例使用。跨数据库 join、跨数据库事务、读写副本路由都不是通用 Driver 自然附送的能力;需要专门契约。副本读取必须明确延迟与读己之写要求,不能在调用者未知的情况下把事务读取发送给副本。 - -## 12. 运行、生成与功能完整性 - -内部只需要围绕实际职责组织:schema 分析与生成、类型化表达式与选择、SQL 编译和关系装配、会话执行、迁移。先作为一个产品保持一致入口;物理包拆分由可选驱动依赖和平台隔离的实际需要决定,不先设计大量对外插件协议。 - -生成成本主要取决于模型字段数、关系数和生成 API 规模。每个表生成必要字段、解码器、创建参数和关系句柄;不生成全部 select/include 组合。schema 根文件显式指定,只追踪相关输入。先测量标准 build_runner 的 watch 路径,再决定是否需要自有 CLI;不能宣称自研天然更快。[^6] - -编译 SQL 与解码对象可以缓存,但必须有容量边界并考虑 schema/driver 类型变化。查询默认参数化,prepared statement 缓存按连接归属。不要默认全局结果缓存、隐式 identity map、自动 flush 或任意实体脏检查;这些会增加一致性与分配成本。 - -Flutter `watch()` 建议定位为已知写入后的查询失效和重取。相同事务中的变化提交后合并通知,rollback 不产生提交事件。SQLite 独立外部连接的变更、PostgreSQL 其他进程写入,并不会自动变成完整的跨进程响应式数据;需要显式通知、轮询或 CDC 方案。基础 ORM 不承担离线同步和冲突解决系统。 - -大结果的 `Stream` 必须绑定真实读取进度和会话释放,不把已经加载到内存的 List 包装成 Stream 后声称流式。HTTP 驱动是否缓冲整包、SQLite 是否逐步读取,以及消费者取消后的资源清理,分别测量。 - -建议提供 `compile()`/`explain()` 和执行观察:SQL 模板、选列、内部关联键、策略、语句数量、参数分块、池等待、数据库耗时、解码耗时及结果行数。数据库 EXPLAIN ANALYZE 可能执行语句,必须与只展示计划区别;日志观察默认不额外发数据库查询。 - -| 能力域 | 完整设计所需内容 | 首批落地顺序 | -| --- | --- | --- | -| 声明 | 默认值、复合键、unique/check、索引、列映射、codec | 首批 | -| 查询 | 条件、排序、分页、标量/Record/DTO 选择 | 首批 | -| 关系 | 正反向、自关联、多对多、按父分页、any/count | 首批核心,复杂策略随后 | -| 写入 | 创建、更新、删除、原子表达式、批量、upsert、returning | 首批 | -| 事务 | 显式会话、rollback、savepoint、生命周期 | 首批 | -| 迁移 | 线性历史、SQL、snapshot、检查和升级测试 | 首批 | -| 驱动 | PostgreSQL、SQLite native;明确配置与关闭 | 首批 | -| 高级 SQL | 聚合、having、join、子查询、CTE、窗口、union | 完整版逐步补齐,保留 raw SQL | -| 运行可靠性 | 真实取消、未知提交、重试策略、连接池耗尽 | 在生产承诺前完成 | -| 高级迁移 | baseline/drift、复杂重建、可恢复回填、非事务恢复 | 接管生产数据前完成 | -| Flutter/Web | isolate、worker、持久化、旧版本升级、watch | 对应平台发布前完成 | -| 特有能力 | PG JSON/array/COPY/锁、SQLite FTS 等 | 按真实需求扩展 | -| 非首版目标 | 分布式事务、同步引擎、任意对象图跟踪、通用查询优化器 | 单独评估 | - -“功能健全”是有明确能力边界和完整数据生命周期,不意味着第一天实现每种数据库的每个扩展。高级 SQL 出口应从开始就存在,但不能永久用它替代已经承诺的常见类型化功能。 - -## 13. 证据、成本与后续验证 - -### 13.1 已完成的语言原型 - -可复查的合并原型为 [type_feasibility.dart](/Users/seven/workspace/dart-orm/research/type_feasibility.dart)。它显式声明语言版本 3.13,不改变项目的 SDK 配置;需使用 Dart 3.13 或更高 SDK 运行: - -```text -dart analyze research/type_feasibility.dart -dart --enable-asserts research/type_feasibility.dart -``` - -该合并文件已通过 analyzer、JIT 断言运行、AOT 编译及断言执行、JS 编译。另用临时负例验证了六类错误调用:结果字段类型颠倒、直接返回 Expr Record、跨驱动配置、错误后端专用 API、非空字段 set(null)、引用不同类型的键;analyzer 全部拒绝,共产生七条预期诊断。 - -| 原型 | 结果 | 证明范围 | -| --- | --- | --- | -| 主构造+字段 metadata+dot shorthand | analyzer/运行/AOT 编译通过 | 稳定 Dart 语法可用 | -| Record 字段 metadata | analyzer 通过 | 可作为生成器输入节点 | -| 同形 Record alias | 可互相赋值;别名构造调用被拒绝 | 必须独立保存表身份 | -| sealed Change 与 `.set(null)` | 分析和运行通过 | 未提供/显式 null 可区分 | -| 复合关系键 selector | 正例通过;错误类型目标被拒绝 | 部分键形状错误可静态拦截 | -| 任意命名 Record/DTO/scalar 选择 | 分析、JIT、AOT 断言运行通过;JS 编译通过 | 选择与解码的类型组合成立 | -| 整体 optional、异构关联列表 | 合成数据断言通过 | 嵌套结果类型和解码组合成立 | -| 假数据 read 发现列 | 反例证明会漏选 | 不能用于运行时列规划 | -| typed 驱动配置与后端扩展 | 正例分析运行通过;两个负例被拒绝 | 可以静态阻止跨驱动配置和特有 API 误用 | - -以上没有连接真实数据库,没有实现 SQL 编译器、schema generator、迁移器或完整驱动。JS 编译不等于浏览器运行,AOT 程序也不等于 Flutter 应用验证。 - -### 13.2 尚需最先攻克的四个实验 - -1. **声明生成实测。** 同一组 User/Post/Profile/Follow,分别使用 Record、主构造 class、类式 table。生成完全相同的 API,测字段修改、重命名、错误位置和复杂约束。验证受限 selector AST,而不只验证签名。 -2. **真实投影和关系执行。** 同一 Selection 在 PostgreSQL 和 SQLite 读取两个列、可空单关联、每父三条子记录。比较批量与单语句结果,覆盖复合键和参数分块。 -3. **事务失败实测。** 外层回滚、savepoint、并发冲突、取消、连接中断、提交未知。证明数据库状态和连接复用,不只看 Future 是否报错。 -4. **迁移升级实测。** 空库重放、每个支持旧版本升级、rename 保数据、SQLite 关联对象重建、PG 非事务索引中断恢复、baseline 与 drift。 - -### 13.3 性能评价方法 - -用同一驱动、同一 SQL、相同结果形状与相同数据比较:裸驱动、完整行 ORM、字段投影 ORM、批量关系、数据库聚合。固定本地与远程 RTT 场景;记录 query count、字节数、p50/p95、吞吐、内存、解码分配和池等待。生成基准覆盖 10/100/1000 模型的冷构建、改单字段增量构建、analyze 与 IDE 补全。 - -100 个用户、每人 10 篇文章和 5 个角色,平铺双列表 join 在这些基数假设下约产生 5000 行;按关系分开约为 100+1000+500 行。这只是行数算例,不是速度结论。少往返和少重复数据之间的选择必须结合 RTT、行宽、索引与数据库 CPU。 - -当前不声称 Record 比 class 更快、不声称比 Drift 更快,也不为生成器或 ORM 设置未经测量的性能百分比。最终默认策略由上述真实实验决定。 - -## 14. 最终取舍与讨论顺序 - -最值得优先验证的是 **Record 数据声明+独立表身份+强类型关系边+可组合 Selection**。它比只在 class 上堆注解更贴近“数据定义一次、操作形状按需产生”的目标,同时能利用 Dart 已稳定的语言能力。 - -这仍是有依据的候选选择:如果真实编辑器重命名、元数据提取和大型 schema 体验不足,主构造 class 或类式表声明可能胜出。切换声明前端不应迫使重写关系、查询、驱动和迁移的语义。 - -建议先通过四个实验锁定最难的语言与执行边界,再定最终方法名称与文件组织。需要继续讨论的重点依次是:Record 能否成为默认声明、关系命名和约束是否足够简短、字段组合映射的书写成本、驱动默认行为与平台优先级。可以保留未来选择,但不同时开工多个平行产品。 - -## 来源 - -以下均为官方语言、数据库或项目维护者资料。未标注发布日期的持续更新文档按本报告研究日期读取;发布者基准与 RC/Preview 功能不作为本 ORM 已验证性能或稳定性的证据。 - -[^1]: Dart team. [Announcing Dart 3.13](https://dart.dev/blog/announcing-dart-3-13), 2026-08-12;[Primary constructors](https://dart.dev/language/primary-constructors). -[^2]: Dart team. [Dot shorthands](https://dart.dev/language/dot-shorthands). -[^3]: Dart team. [Records](https://dart.dev/language/records). -[^4]: Dart team. [Extension types](https://dart.dev/language/extension-types). -[^5]: Dart team. [An update on Dart macros & data serialization](https://dart.dev/blog/an-update-on-dart-macros-data-serialization), 2025-01-29. -[^6]: Dart team. [build_runner](https://dart.dev/tools/build_runner). -[^7]: Flutter team. [Does Flutter come with a reflection / mirrors system?](https://docs.flutter.dev/resources/faq#does-flutter-come-with-a-reflection--mirrors-system). -[^8]: Dart team. [Methods: Operators](https://dart.dev/language/methods#operators);[Non-bool operands](https://dart.dev/tools/diagnostics/non_bool_operand). -[^9]: Drizzle team. [Drizzle relations](https://orm.drizzle.team/docs/relations), 所查页面使用 RC 安装示例。 -[^10]: Dart team. [Built-in types: Numbers](https://dart.dev/language/built-in-types#numbers). -[^11]: PostgreSQL Global Development Group. [Numeric Types](https://www.postgresql.org/docs/18/datatype-numeric.html), PostgreSQL 18. -[^12]: SQLite project. [Datatypes In SQLite](https://www.sqlite.org/datatype3.html). -[^13]: SQLite project. [STRICT Tables](https://www.sqlite.org/stricttables.html). -[^14]: Drift project. [Custom queries](https://drift.simonbinder.eu/sql_api/custom_queries/). -[^15]: SQLAlchemy authors. [Relationship Loading Techniques](https://docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html), SQLAlchemy 2.0. -[^16]: Kysely project. [Nested array](https://kysely.dev/docs/examples/select/nested-array). -[^17]: PostgreSQL Global Development Group. [Transaction Isolation](https://www.postgresql.org/docs/18/transaction-iso.html). -[^18]: Drift project. [Transactions](https://drift.simonbinder.eu/dart_api/transactions/). -[^19]: PostgreSQL Global Development Group. [SAVEPOINT](https://www.postgresql.org/docs/18/sql-savepoint.html). -[^20]: SQLite project. [Savepoints](https://www.sqlite.org/lang_savepoint.html). -[^21]: SQLite project. [Isolation In SQLite](https://www.sqlite.org/isolation.html). -[^22]: SQLite project. [Transaction](https://www.sqlite.org/lang_transaction.html). -[^23]: PostgreSQL Global Development Group. [Serialization Failure Handling](https://www.postgresql.org/docs/18/mvcc-serialization-failure-handling.html). -[^24]: Dart team. [Future.timeout](https://api.dart.dev/dart-async/Future/timeout.html). -[^25]: SQLite project. [Set A Busy Timeout](https://www.sqlite.org/c3ref/busy_timeout.html). -[^26]: SQLite project. [Interrupt A Long-Running Query](https://www.sqlite.org/c3ref/interrupt.html). -[^27]: Prisma. [Migration histories](https://docs.prisma.io/docs/orm/v7/prisma-migrate/understanding-prisma-migrate/migration-histories). -[^28]: Drift project. [Migrations](https://drift.simonbinder.eu/migrations/). -[^29]: Drift project. [Migration tests](https://drift.simonbinder.eu/migrations/tests/). -[^30]: PostgreSQL Global Development Group. [CREATE INDEX](https://www.postgresql.org/docs/18/sql-createindex.html). -[^31]: PostgreSQL Global Development Group. [Explicit Locking](https://www.postgresql.org/docs/18/explicit-locking.html). -[^32]: SQLite project. [ALTER TABLE](https://www.sqlite.org/lang_altertable.html). -[^33]: SQLite project. [Foreign Key Support](https://www.sqlite.org/foreignkeys.html). -[^34]: Prisma. [Baselining a database](https://www.prisma.io/docs/orm/prisma-migrate/workflows/baselining). -[^35]: PostgreSQL Dart maintainers. [postgres package](https://pub.dev/packages/postgres), 3.5.12 at retrieval. -[^36]: PostgreSQL Dart maintainers. [ConnectionSettings](https://pub.dev/documentation/postgres/latest/postgres/ConnectionSettings-class.html). -[^37]: PostgreSQL Dart maintainers. [PoolSettings](https://pub.dev/documentation/postgres/latest/postgres/PoolSettings-class.html). -[^38]: sqlite3 Dart maintainers. [sqlite3 package](https://pub.dev/packages/sqlite3), 3.6.0 at retrieval;[CommonDatabase](https://pub.dev/documentation/sqlite3/latest/common/CommonDatabase-class.html). -[^39]: pg Dart maintainers. [pg package](https://pub.dev/packages/pg), 发布者性能材料未独立复现。 -[^40]: PostgreSQL Dart maintainers. [SslMode](https://pub.dev/documentation/postgres/latest/postgres/SslMode.html). -[^41]: Drift project. [Isolates](https://drift.simonbinder.eu/isolates/). -[^42]: SQLite project. [In-Memory Databases](https://www.sqlite.org/inmemorydb.html). -[^43]: SQLite project. [Write-Ahead Logging](https://www.sqlite.org/wal.html). -[^44]: Drift project. [Web](https://drift.simonbinder.eu/platforms/web/). -[^45]: mysql_client_plus maintainers. [mysql_client_plus package](https://pub.dev/packages/mysql_client_plus), 驱动候选,未进行独立兼容验证。 -[^46]: Turso. [SQL over HTTP quickstart](https://docs.turso.tech/sdk/http/quickstart);[TypeScript SDK reference](https://docs.turso.tech/sdk/ts/reference). -[^47]: Neon. [Serverless driver 1.0.0](https://neon.com/blog/serverless-driver-ga), JavaScript/TypeScript 驱动资料,仅用于传输设计参考。 -[^48]: SQLite project. [UPDATE](https://www.sqlite.org/lang_update.html). -[^49]: SQLite project. [RETURNING](https://www.sqlite.org/lang_returning.html). diff --git a/research/prisma-v8-reassessment.md b/research/prisma-v8-reassessment.md deleted file mode 100644 index 3601c788..00000000 --- a/research/prisma-v8-reassessment.md +++ /dev/null @@ -1,233 +0,0 @@ -# Prisma 8 reassessment - -Research date: 2026-09-19. This report separates verified upstream behavior -from decisions for this Dart package. It supplements the original design; -it does not adopt Prisma's API or artifact format as this package's contract. - -## Verified release state - -Prisma 8 is a **release candidate**, with general availability expected in -October 2026. Live npm registry reads on the research date returned: - -| Package | `latest` | -| --- | --- | -| `prisma` CLI | `8.0.0-rc.15` | -| `@prisma/orm-postgres` | `8.0.0-rc.11` | - -The CLI and database libraries are versioned separately. A headline saying -“Prisma 8 is here” does not mean all version 7 features have shipped in 8. - -Sources: [release status](https://www.prisma.io/docs/orm/release-status), -[CLI registry tags](https://registry.npmjs.org/-/package/prisma/dist-tags), -[PostgreSQL registry tags](https://registry.npmjs.org/-/package/@prisma%2form-postgres/dist-tags). - -| Database | Prisma 8 status on this date | -| --- | --- | -| PostgreSQL | Release candidate | -| MongoDB | Early access; ORM transactions are not available | -| SQLite | Experimental package | -| MySQL | Planned next; no Prisma 8 library yet | -| MariaDB, SQL Server, CockroachDB | Planned; no Prisma 8 library yet | - -This is the version 8 matrix, not the older version 7 compatibility matrix. -Hosted PostgreSQL services use the PostgreSQL connection path; hosting does -not establish support for every extension or PostgreSQL-compatible engine. -Source: [supported databases](https://www.prisma.io/docs/orm/supported-databases). - -The old `prisma-next` repository is archived. Current source resolves to -[`prisma/orm`](https://github.com/prisma/orm), including requests through the -`prisma/prisma` redirect. Source inspection below is pinned to commit -`ad23f6f47f7964a12f07899c39d2449f1d5b99ea` (2026-09-18), which is a main-branch -snapshot rather than proof that every source feature is in the published RC. - -## What the architecture actually provides - -Prisma separates a model/storage contract, query construction, runtime -execution, target-specific SQL behavior, driver I/O, and migration tooling. -ORM queries, SQL-builder queries, and raw SQL converge on an inspectable -plan. The runtime executes that plan; a driver manages database access. -Authoring and migration code are kept out of runtime dependencies. The -repository checks import boundaries rather than relying on folder names. - -The application-facing PostgreSQL package has public subpaths for -`contract`, `contract-builder`, `builder`, `orm-client`, `family-runtime`, -`adapter`, `driver`, `migration`, and `config`. Its own description presents -it as the one package a PostgreSQL application installs. Internal packages -and public subpaths are different concepts: independent use does not -require making application developers assemble dozens of packages. - -Sources: [architecture](https://github.com/prisma/orm/blob/ad23f6f47f7964a12f07899c39d2449f1d5b99ea/ARCHITECTURE.md), -[dependency boundaries](https://github.com/prisma/orm/blob/ad23f6f47f7964a12f07899c39d2449f1d5b99ea/docs/architecture%20docs/Package-Layering.md), -[public package exports](https://github.com/prisma/orm/blob/ad23f6f47f7964a12f07899c39d2449f1d5b99ea/packages/9-public/%40prisma/orm-postgres/package.json). - -Two details qualify the older announcement's claims: - -- The current PostgreSQL runtime reference says `for await` still loads all - rows first. An async iterable alone is not evidence of bounded memory. -- The default `verifyMarker: 'onFirstUse'` reports a contract mismatch as a - warning and continues. A marker check is also not a live catalog audit. - -Source: [transactions and runtime reference](https://www.prisma.io/docs/orm/reference/transactions-and-runtime). -Our existing explicit sessions, backpressure, cancellation, and capability -checks should retain their measured guarantees through this refactor. - -## Model declaration and typed use - -Prisma accepts either its schema language or a TypeScript builder. Both -produce a deterministic contract JSON file plus TypeScript declarations. -The TypeScript builder supports composition and data-driven declarations; -relations are added through model handles. The documentation still -recommends the schema language for its brevity when composition is not -needed. This is not evidence that the TypeScript object syntax transfers -unchanged to Dart. - -Source: [TypeScript contract authoring](https://www.prisma.io/docs/orm/contract-authoring/typescript-schema-builder). - -The query surfaces deliberately separate model names (`db.orm.public.User`) -from table names (`db.sql.public.user`). Selections determine result types. -Raw queries supply column codecs or an explicit result shape. This gives -applications an escape route for SQL that the ORM cannot express while -preserving value parameterization and decoding. - -Sources: [ORM client](https://www.prisma.io/docs/orm/reference/orm-client), -[SQL query builder](https://www.prisma.io/docs/orm/reference/sql-query-builder), -[raw queries](https://www.prisma.io/docs/orm/reference/raw-queries). - -### Dart decision - -Support an ordinary immutable **Dart class** as a first-class model source. -Fields keep their Dart types and annotations; a validated constructor gives -the generator a direct way to reconstruct the class. Generate typed column -handles, creates, patches, and relationships from that source. Preserve -record declarations as a compact alternative and records as projection -results. Both declaration forms must produce the same runtime semantics. - -Important boundaries: - -- A model class describes values. It does not own a connection, issue SQL - from getters, or hide lazy relation loading. -- Table identity remains explicit and independent of a class or record - type. Two tables may store the same Dart shape. -- Constructor mapping is checked during generation. Unsupported constructor - shapes must produce a source-located error, not a runtime cast failure. -- Typed selectors use generated Dart members. Do not translate Prisma's - string-key projections into stringly typed Dart maps or claim that Dart - can infer new record members from arbitrary runtime data. -- Low-level query construction accepts typed table/column declarations - without requiring a model class or code generation. -- All authoring forms converge on one storage description. Data-driven - configuration is not a second competing schema or a parallel execution - implementation. - -These are design choices for Dart and this repository, not Prisma features. -Static generation is useful here; eliminating all generated Dart would work -against directly typed members and predictable AOT/Flutter behavior. - -## Six useful layers, one product package - -Use independently importable public libraries with tested import boundaries. -The following are responsibilities, not a requirement to create six Pub -packages or a plugin registration framework. - -| Layer | Responsibility | Must work independently | -| --- | --- | --- | -| Values and contract | SQL value codecs, errors, engine capabilities, storage description | Use values and describe storage without a driver or ORM | -| SQL construction | Typed columns, expressions, selections, compilation | Build and inspect parameterized SQL without a connection or generated model | -| Driver and runtime | Connections, leases, cursors, transactions, cancellation | Execute parameterized SQL without ORM/model imports | -| ORM | Typed model queries, relation loading, creates and patches | Add model convenience over the same SQL/runtime behavior | -| Schema and migrations | Authoring, frozen snapshots, catalog checks, planning and history execution | Use migration tools with driver/runtime without generated application models | -| CLI | Project discovery, arguments, display, exit status | Call public tooling APIs; contain no second migration implementation | - -Dependencies point toward shared value/contract types. SQL construction and -driver I/O are peers consuming the same small statement/result contracts; -neither should depend on ORM. ORM composes them. Migrations consume storage -and execution interfaces, not the application's live model objects. CLI -consumes migration/generation services. Keep analysis and filesystem imports -out of runtime libraries. - -Before calling this modularity complete, prove concrete independent usage: -compile SQL without opening a database; execute driver SQL without importing -`orm.dart`; run migrations without generated entities; and import the -portable runtime on Web without pulling native or CLI libraries. A facade -that merely hides names while all files remain in one Dart `part` library -does not establish independent implementation boundaries. - -## Migrations: borrow checks, keep Dart artifacts - -Prisma migrations have start/end contract hashes and form a graph. In the -current release candidate, branch reconciliation needs explicit migrations -from each branch state to the merged state; squash and split are not yet -implemented. The live database marker records a state, while the ledger -records executed migrations. Reading the marker does not inspect the actual -tables. - -Source: [migration graph and its current limits](https://www.prisma.io/docs/orm/migrations/the-migration-graph). - -Prisma authors `migration.ts` but executes compiled operations from -`ops.json`. Historical snapshots type its data transformations. Prechecks -and postchecks support verification and recovery, but recompilation is -required after a source edit. PostgreSQL currently executes a migration run -inside one transaction, so this route cannot execute `CREATE INDEX -CONCURRENTLY`. - -Source: [editing a migration](https://www.prisma.io/docs/orm/migrations/editing-a-migration). - -For this package, retain **Dart source snapshots and migration definitions**, -static registration, frozen historical types, reviewed fingerprints, and -one engine per history. Borrow inspectable operations, explicit preconditions, -catalog verification, and precise recovery states. Do not restore the -rejected JSON workflow or add a migration graph without a demonstrated need. -Schema rollback also cannot promise recovery of deleted application data. - -Database support changes migration execution semantics. SQLite rebuilds, -PostgreSQL transactional DDL, and MySQL/MariaDB implicit DDL commits need -their own checks and recovery behavior. A shared verb must not advertise -identical atomicity across those engines. - -## CLI: intent and predictable output - -Prisma separates offline contract emission and migration planning from -database initialization, verification, and migration execution. `migration -plan` reports an unknown origin instead of silently generating full creation -steps when existing history makes that assumption unsafe. Global flags -support explicit output formats, config paths, noninteractive operation, -help, and structured diagnostics. - -Sources: [migration plan](https://www.prisma.io/docs/cli/migration-plan), -[global flags](https://www.prisma.io/docs/cli/global-flags). - -For Dart, expose a small, discoverable command tree: project setup, model -generation, schema inspection, migration plan/check/status/apply. Keep -preview and execution distinct, retain source paths in errors, never print -credentials, reject unknown options, and return stable nonzero exit codes -for misuse or failed checks. Help should contain the next useful command. -Structured CLI output is optional presentation; it does not make JSON a -schema or migration storage format. - -## Database delivery decision - -The user has requested real SQLite, PostgreSQL, MySQL, and MariaDB support -and acceptance. Implement and validate each; do not inherit Prisma 8's -current database limits or claim compatibility based only on a shared wire -protocol. - -MySQL and MariaDB can share a transport implementation where justified, -while identifying the actual server and retaining distinct capability and -migration decisions. Acceptance must cover real values, transactions, -rollback, generated keys, nulls, large integers/decimals, temporal values, -errors, and schema evolution. SQLite Web remains a separately verified -platform within the SQLite implementation. SQL Server and document stores -remain outside the accepted implementation scope until explicitly added. - -## Adoption checklist - -Adopt: one package with usable lower layers; model/storage identity -separation; class and record authoring through one generator; common SQL -execution contracts; visible query plans; engine-specific capabilities; -small CLI workflows with inspectable results. - -Do not copy: the JSON artifact requirement, TypeScript type-level syntax, -the full multidimensional package/SPI topology, eager claims of streaming, -warning-only verification, or roadmap features presented as delivered -database support. Preserve existing correctness guarantees while reducing -the amount of ORM machinery a low-level caller must import or understand. diff --git a/research/standalone-orm-redesign.md b/research/standalone-orm-redesign.md deleted file mode 100644 index 88b35e43..00000000 --- a/research/standalone-orm-redesign.md +++ /dev/null @@ -1,67 +0,0 @@ -# Standalone Dart ORM redesign - -Decision date: 2026-09-19. This is a development redesign, not a compatibility -release. The user's accepted database scope is SQLite (including Flutter/Web), -PostgreSQL, MySQL and MariaDB. The implementation remains one product package. - -## Product contract - -- Declare an immutable Dart model once; generated reads return that nominal type. - Records remain useful for projections and explicit structural data shapes. -- Keep model rows, insert inputs, patches and relation projections distinct. - Generated IDs are required in a persisted row but omitted from create inputs; - updates distinguish keeping a value from writing SQL NULL. -- Every layer can be used without the layer above it. This is an import-graph - requirement, not merely a filtered export list. -- Preserve bound parameters, identifier quoting, alias scope checks, explicit - transactions and observable relationship query counts. -- A database engine is selected explicitly. Capabilities and physical schema - behavior reflect that engine, including differences between MySQL and MariaDB. -- Current schema, configuration and immutable migration history are Dart source. - No compatibility reader for retired artifact formats is introduced. - -## Library boundaries - -| Library | Responsibility | Must not depend on | -| --- | --- | --- | -| `values.dart` | Codecs, exact numbers, calendar values and errors | Drivers, queries, models, tooling | -| `driver.dart` | Commands, rows, cursors, connection leases and capabilities | ORM, schema generation, migrations | -| `schema_model.dart` | Physical columns, keys, indexes and constraints | Connections, ORM, analyzer | -| `schema.dart` | Typed application declarations | ORM execution and generation | -| `runtime.dart` | Raw execution, sessions, transactions, cancellation and observations | Query builder, generated models | -| SQL construction | Typed expressions, query descriptions, compilation and decoding plans | Concrete drivers and connected databases | -| `orm.dart` | Bound typed queries, relationship loading and subscriptions | Platform drivers and development tools | -| `migrate.dart` | Frozen histories, diff, catalog verification and execution | Application models and ORM queries | -| Generation/CLI | Static analysis, reproducible source output and project commands | Application runtime initialization for offline work | - -Concrete adapters consume the driver contract. Convenience entrypoints may -compose adapters with the ORM, but a separate direct driver import must retain -the lower-level dependency boundary. A single package still resolves its declared -development dependencies; independent imports do not imply separate pub downloads. - -## Implementation sequence - -1. Extract values, physical metadata, driver contracts and the existing transaction - state machine into actual libraries. Keep one transaction implementation. -2. Read primary-constructor models directly and materialize the user's class in - generated typed selections. Preserve table identity independently of row type. -3. Make SQL construction and compilation usable without opening a database or - constructing a fake driver. Preserve alias scope and explicit session ownership. -4. Implement MySQL/MariaDB adapters, SQL dialects, catalog and migration behavior. - Treat nontransactional DDL, generated IDs, returning, exact values and errors - explicitly; do not advertise SQLite/PostgreSQL semantics for these engines. -5. Provide a coherent CLI onboarding/configuration/generation/migration workflow - using typed Dart configuration and static history imports. -6. Update user-facing examples and boundary documentation. Verify isolated layer - consumers, negative type cases and actual database/platform behavior. -7. Commit reviewable changes, push, open a PR against `main`, request Codex review, - address findings, and merge only the verified reviewed head. - -## Acceptance evidence - -Required evidence includes import-graph checks, standalone layer consumers, -nominal model CRUD/relations/projections, invalid types rejected by analysis, -CLI setup through migration replay, real SQLite/PostgreSQL/MySQL/MariaDB tests, -and refreshed SQLite Web assets with Dart and Flutter JS/WASM acceptance. -Existing validation records remain historical baselines. New records identify -the tested versions and any unsupported feature or deployment target explicitly. diff --git a/research/type_feasibility.dart b/research/type_feasibility.dart deleted file mode 100644 index 0f90b7d1..00000000 --- a/research/type_feasibility.dart +++ /dev/null @@ -1,317 +0,0 @@ -// @dart = 3.13 -// Standalone Dart 3.13 language/type experiment for the accompanying report. -// Synthetic rows only. This is not an ORM, SQL compiler, or database test. -// Run: dart --enable-asserts research/type_feasibility.dart - -// Dart 3.13.3 proof of type inference and deterministic selection/decode separation. -// No database, SQL compiler, query planner, or ORM is implemented here. -typedef Row = Map; -typedef Read = T Function(Expr expression); - -abstract interface class Selection { - List get plan; - T decode(Row row); -} - -final class Expr implements Selection { - const Expr(this.name, this.codec); - final String name; - final T Function(Object?) codec; - @override - List get plan => [name]; - @override - T decode(Row row) => codec(row[name]); -} - -final class Mapped implements Selection { - const Mapped(this.plan, this.mapper); - @override - final List plan; - final T Function(Row) mapper; - @override - T decode(Row row) => mapper(row); -} - -extension PairProjection on (Selection, Selection) { - Selection map(R Function(A, B) mapper) => Mapped([ - ...$1.plan, - ...$2.plan, - ], (row) => mapper($1.decode(row), $2.decode(row))); -} - -extension TripleProjection - on (Selection, Selection, Selection) { - Selection map(R Function(A, B, C) mapper) => Mapped([ - ...$1.plan, - ...$2.plan, - ...$3.plan, - ], (row) => mapper($1.decode(row), $2.decode(row), $3.decode(row))); -} - -Selection optional(String presence, Selection item) => Mapped([ - presence, - ...item.plan, -], (row) => row[presence] == null ? null : item.decode(row)); - -Selection> many(String key, Selection item) => Mapped( - ['$key[${item.plan.join(',')}]'], - (row) => switch (row[key]) { - final List rows => rows.map(item.decode).toList(), - _ => throw FormatException('Expected nested rows at $key'), - }, -); - -int integer(Object? value) => switch (value) { - final int result => result, - _ => throw const FormatException('Expected integer'), -}; -String string(Object? value) => switch (value) { - final String result => result, - _ => throw const FormatException('Expected text'), -}; - -final class Users { - final id = const Expr('user.id', integer); - final email = const Expr('user.email', string); -} - -final class Posts { - final id = const Expr('post.id', integer); - final title = const Expr('post.title', string); -} - -final class Card { - const Card(this.id, this.email); - final int id; - final String email; -} - -Selection select(Selection Function(Users) build) => build(Users()); - -R rowReader(Users u, Row row, R Function(Users, Read) mapper) { - T read(Expr expression) => expression.decode(row); - return mapper(u, read); -} - -void verifySelections() { - final u = Users(); - final p = Posts(); - var mapperCalls = 0; - final named = select( - (u) => (u.id, u.email).map((id, email) { - mapperCalls++; - return (identifier: id, contact: email); - }), - ); - assert(mapperCalls == 0); // Plan collection never executes a row callback. - final Selection<({int identifier, String contact})> checkedNamed = named; - final row = {'user.id': 1, 'user.email': 'a@example.com'}; - final decoded = checkedNamed.decode(row); - assert(mapperCalls == 1 && decoded.identifier == 1); - assert(named.plan.join(',') == 'user.id,user.email'); - - final Selection scalar = select((u) => u.id); - assert(scalar.decode(row) == 1); - final Selection dto = select((u) => (u.id, u.email).map(Card.new)); - assert(dto.decode(row).email == 'a@example.com'); - - final author = optional('user.id', (u.id, u.email).map(Card.new)); - final Selection checkedAuthor = author; - assert(checkedAuthor.decode({'user.id': null, 'user.email': null}) == null); - assert(checkedAuthor.decode(row)?.id == 1); - - final titles = many('posts', p.title); - final authors = many('friends', (u.id, u.email).map(Card.new)); - final graph = ( - u.id, - titles, - authors, - ).map((id, titles, friends) => (id: id, titles: titles, friends: friends)); - final Selection<({int id, List titles, List friends})> - checkedGraph = graph; - final decodedGraph = checkedGraph.decode({ - ...row, - 'posts': [ - {'post.title': 'Hello'}, - ], - 'friends': [ - {'user.id': 2, 'user.email': 'b@example.com'}, - ], - }); - assert(decodedGraph.titles.single == 'Hello'); - assert(decodedGraph.friends.single.id == 2); - - // Reader callback independently proves named Record inference, only for decode. - final readRecord = rowReader( - u, - row, - (u, read) => (account: read(u.id), address: read(u.email)), - ); - final ({int account, String address}) checkedReadRecord = readRecord; - assert(checkedReadRecord.account == 1); - - // Counterexample: placeholder discovery misses conditionally accessed columns. - final selected = []; - T fakeRead(Expr expression) { - selected.add(expression.name); - return expression.codec(switch (expression.name) { - 'user.id' => 0, - 'user.email' => '', - _ => throw StateError('No valid dummy value'), - }); - } - - String conditional(Read read) => read(u.id) > 0 ? read(u.email) : 'hidden'; - conditional(fakeRead); - assert(!selected.contains('user.email')); - assert(rowReader(u, row, (_, read) => conditional(read)) == 'a@example.com'); - print( - 'PASS: arbitrary named record, single scalar, DTO constructor tear-off,', - ); - print('optional whole object, heterogeneous nested relation selections,'); - print('generic reader callback inference, and dummy-reader counterexample.'); - print('Record plan: ${named.plan}'); - print('Nested plan (structural labels, not SQL): ${graph.plan}'); - print('Dummy reader discovered only: $selected'); -} - -// Language feasibility only. No database driver is implemented here. -sealed class Backend {} - -final class Pg extends Backend {} - -final class Sqlite extends Backend {} - -enum TlsMode { verifyFull, disable } - -enum Journal { wal, rollback } - -final class PgOptions({ - required final Uri url, - final TlsMode tls = .verifyFull, - final int maxConnections = 8, -}); - -final class SqliteOptions.file( - final String path, { - final Journal journal = .wal, -}); - -abstract interface class Driver { - String get description; -} - -final class PgDriver(final PgOptions options) implements Driver { - @override - String get description => 'postgres'; -} - -final class SqliteDriver(final SqliteOptions options) - implements Driver { - @override - String get description => 'sqlite'; -} - -final class AppDatabase { - AppDatabase._(this.driver); - final Driver driver; - - static Future> open( - Driver driver, - ) async { - return AppDatabase._(driver); - } -} - -extension PgFeatures on AppDatabase { - String get advisoryLockFeature => 'pg_advisory_xact_lock'; -} - -Future verifyDrivers() async { - final pg = await AppDatabase.open( - PgDriver(.new(url: Uri.parse('postgresql://localhost/example'))), - ); - final sqlite = await AppDatabase.open( - SqliteDriver(.file('/tmp/example.sqlite')), - ); - AppDatabase typedPg = pg; - AppDatabase typedSqlite = sqlite; - assert(typedPg.advisoryLockFeature == 'pg_advisory_xact_lock'); - assert(typedSqlite.driver.description == 'sqlite'); - print('PASS: typed driver options and backend-specific extension'); -} - -// Schema and patch language probes. No schema extraction is implemented. -final class Id { - const Id.generated(); -} - -final class Unique { - const Unique(); -} - -typedef User = ({ - @Id.generated() int id, - @Unique() String email, - String? nickname, -}); -typedef OtherRow = ({int id, String email, String? nickname}); - -sealed class Change { - const Change(); - const factory Change.keep() = Keep; - const factory Change.set(T value) = SetValue; -} - -final class Keep extends Change { - const Keep(); -} - -final class SetValue extends Change { - const SetValue(this.value); - final T value; -} - -String describeChange(Change change) => switch (change) { - Keep() => 'keep', - SetValue(:final value) => 'set:$value', -}; - -final class Entity { - const Entity(this.name); - final String name; - EntityKey key(K Function(M) selector) => EntityKey(this, selector); -} - -final class EntityKey { - const EntityKey(this.entity, this.selector); - final Entity entity; - final K Function(M) selector; - String references(EntityKey target) => - '${entity.name}->${target.entity.name}'; -} - -void verifyDeclarations() { - final OtherRow other = (id: 1, email: 'a@example.com', nickname: null); - final User sameShape = other; // Aliases do not create entity identity. - final users = Entity('users'); - final others = Entity('others'); - assert(!identical(users, others)); - assert(sameShape.email == 'a@example.com'); - assert(describeChange(.keep()) == 'keep'); - assert(describeChange(.set(null)) == 'set:null'); - assert(describeChange(.set('seven')) == 'set:seven'); - final link = users - .key((u) => (u.id, u.email)) - .references(others.key((u) => (u.id, u.email))); - assert(link == 'users->others'); - print( - 'PASS: record metadata, structural identity, typed keys and tri-state patch', - ); -} - -Future main() async { - verifySelections(); - verifyDeclarations(); - await verifyDrivers(); -} diff --git a/research/validation/api-audit-browser-js.log b/research/validation/api-audit-browser-js.log deleted file mode 100644 index c8e0f5a6..00000000 --- a/research/validation/api-audit-browser-js.log +++ /dev/null @@ -1,90 +0,0 @@ -Running build hooks...Running build hooks...{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 66 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 41 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 17 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 21 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 19 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 34 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 27 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 3 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 2 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 4 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 4 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 160 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 31 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 103 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 3 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 22 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "js", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/api-audit-browser-wasm.log b/research/validation/api-audit-browser-wasm.log deleted file mode 100644 index 06fa1544..00000000 --- a/research/validation/api-audit-browser-wasm.log +++ /dev/null @@ -1,90 +0,0 @@ -Running build hooks...Running build hooks...{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 56 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 40 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 14 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 19 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 24 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 159 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 31 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 101 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 14 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "wasm", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/api-audit-native.log b/research/validation/api-audit-native.log deleted file mode 100644 index 17b7c068..00000000 --- a/research/validation/api-audit-native.log +++ /dev/null @@ -1,937 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/client_default_test.dart -00:00 +0: test/client_default_test.dart: client defaults sqlite omitted values use typed Dart factories and return stored records -00:00 +1: test/client_default_test.dart: client defaults sqlite explicit values, null and SQL DEFAULT bypass their client factory -00:00 +2: test/client_default_test.dart: client defaults sqlite prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +3: test/client_default_test.dart: client defaults sqlite batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +4: test/client_default_test.dart: client defaults sqlite updates and conflict updates do not reset omitted client values -00:00 +5: test/client_default_test.dart: client defaults sqlite factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +6: test/client_default_test.dart: client defaults sqlite snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +7: test/client_default_test.dart: client defaults sqlite explicit DEFAULT can choose database identity instead of the client value -00:00 +8: test/client_default_test.dart: client defaults postgres omitted values use typed Dart factories and return stored records -00:00 +9: test/client_default_test.dart: client defaults postgres explicit values, null and SQL DEFAULT bypass their client factory -00:00 +10: test/client_default_test.dart: client defaults postgres prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +11: test/client_default_test.dart: client defaults postgres batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +12: test/client_default_test.dart: client defaults postgres updates and conflict updates do not reset omitted client values -00:00 +13: test/client_default_test.dart: client defaults postgres factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +14: test/client_default_test.dart: client defaults postgres snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +15: test/client_default_test.dart: client defaults postgres explicit DEFAULT can choose database identity instead of the client value -00:00 +16: test/client_default_test.dart: generation reads factory references without executing them and supports moved output -00:03 +17: loading test/decimal_precision_test.dart -00:03 +17: test/decimal_precision_test.dart: decimal precision sqlite column coercion rounds signed ties and handles negative and excess scales -00:03 +18: test/decimal_precision_test.dart: decimal precision sqlite writes constrain columns while arithmetic and wider aggregates retain their result precision -00:03 +19: test/decimal_precision_test.dart: decimal precision sqlite batch inserts and upserts use column coercion and roll back as a unit -00:03 +20: test/decimal_precision_test.dart: decimal precision sqlite catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +21: test/decimal_precision_test.dart: decimal precision sqlite reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:05 +22: test/decimal_precision_test.dart: decimal precision sqlite physical renames and changing constrained defaults preserve column contracts -00:05 +23: test/decimal_precision_test.dart: decimal precision sqlite maximum declared precision and scale bounds remain exact -00:05 +24: test/decimal_precision_test.dart: decimal precision sqlite historical backfills retain constrained decimal key declarations -00:05 +25: test/decimal_precision_test.dart: decimal precision postgres column coercion rounds signed ties and handles negative and excess scales -00:05 +26: test/decimal_precision_test.dart: decimal precision postgres writes constrain columns while arithmetic and wider aggregates retain their result precision -00:05 +27: test/decimal_precision_test.dart: decimal precision postgres batch inserts and upserts use column coercion and roll back as a unit -00:05 +28: test/decimal_precision_test.dart: decimal precision postgres catalogs snapshots and imported declarations retain precision scale and defaults -00:06 +29: test/decimal_precision_test.dart: decimal precision postgres reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:06 +30: test/decimal_precision_test.dart: decimal precision postgres physical renames and changing constrained defaults preserve column contracts -00:06 +31: test/decimal_precision_test.dart: decimal precision postgres maximum declared precision and scale bounds remain exact -00:06 +32: test/decimal_precision_test.dart: decimal precision postgres historical backfills retain constrained decimal key declarations -00:06 +33: test/decimal_precision_test.dart: SQLite enforces stored precision and checks default coercion and trusted-schema behavior -00:06 +34: test/decimal_precision_test.dart: generation validates DecimalDigits and retains custom decimal codec types -00:09 +35: test/decimal_precision_test.dart: metadata rejects invalid precisions scales and non-decimal columns -00:09 +36: loading test/selection_test.dart -00:09 +36: test/selection_test.dart: selection sqlite all typed arities defer mapping and preserve left-to-right evaluation -00:09 +37: test/selection_test.dart: selection sqlite first decoder failure prevents later fields and outer mapper -00:09 +38: test/selection_test.dart: selection sqlite dynamic fields snapshot keys and preserve nulls and mapping order -00:09 +39: test/selection_test.dart: selection postgres all typed arities defer mapping and preserve left-to-right evaluation -00:09 +40: test/selection_test.dart: selection postgres first decoder failure prevents later fields and outer mapper -00:09 +41: test/selection_test.dart: selection postgres dynamic fields snapshot keys and preserve nulls and mapping order -00:09 +42: loading test/build_runner_test.dart -00:09 +42: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -00:38 +43: loading test/schema_version_test.dart -00:38 +43: test/schema_version_test.dart: schema version sqlite invalid ranges fail before SQL; caller transactions stay usable -00:38 +44: test/schema_version_test.dart: schema version sqlite unversioned existing data is rejected without creating history -00:38 +45: test/schema_version_test.dart: schema version sqlite default requires latest; inclusive compatibility range is explicit -00:38 +46: test/schema_version_test.dart: schema version sqlite newer database rejects downgrade without changing data or schema -00:38 +47: test/schema_version_test.dart: schema version sqlite complete applied prefix is checked, including old checksums and missing entries -00:38 +48: test/schema_version_test.dart: schema version sqlite matching version does not claim that the catalog is free of drift -00:38 +49: test/schema_version_test.dart: schema version sqlite verified baseline is compatible without replaying creation -00:38 +50: test/schema_version_test.dart: schema version sqlite borrowed session uses a read snapshot and survives rejected versions -00:38 +51: test/schema_version_test.dart: schema version postgres invalid ranges fail before SQL; caller transactions stay usable -00:38 +52: test/schema_version_test.dart: schema version postgres unversioned existing data is rejected without creating history -00:38 +53: test/schema_version_test.dart: schema version postgres default requires latest; inclusive compatibility range is explicit -00:38 +54: test/schema_version_test.dart: schema version postgres newer database rejects downgrade without changing data or schema -00:38 +55: test/schema_version_test.dart: schema version postgres complete applied prefix is checked, including old checksums and missing entries -00:38 +56: test/schema_version_test.dart: schema version postgres matching version does not claim that the catalog is free of drift -00:38 +57: test/schema_version_test.dart: schema version postgres verified baseline is compatible without replaying creation -00:38 +58: test/schema_version_test.dart: schema version postgres borrowed session uses a read snapshot and survives rejected versions -00:38 +59: test/schema_version_test.dart: schema version postgres unfinished recoverable migration blocks startup until repaired and completed -00:38 +60: test/schema_version_test.dart: schema version postgres catalog and history share a snapshot and the supplied history is frozen -00:39 +61: test/schema_version_test.dart: SQLite version gate works on an explicitly read-only file -00:39 +62: loading test/types_test.dart -00:39 +62: test/types_test.dart: junction-table keys, enum payloads and nested results keep generated types -00:41 +63: test/types_test.dart: computed generated fields are statically readable and never writable -00:41 +64: test/types_test.dart: client-default create parameters preserve domain, nullable and timestamp types -00:42 +65: test/types_test.dart: query-only relation APIs retain result types and expose no graph writes -00:42 +66: test/types_test.dart: calendar APIs reject instants, strings and mixed temporal types -00:43 +67: test/types_test.dart: actual generated APIs reject wrong inputs, results and backend options -00:43 +68: loading test/migration_test.dart -00:43 +68: test/migration_test.dart: (setUpAll) -00:43 +68: test/migration_test.dart: migrations sqlite explicit table and column renames preserve incoming references and data -00:43 +69: test/migration_test.dart: migrations sqlite constraint changes resolve catalog names and retain rows -00:43 +70: test/migration_test.dart: migrations sqlite failed NOT NULL rebuild restores data history and foreign key settings -00:43 +71: test/migration_test.dart: migrations sqlite reviewed conversion expressions preserve values when storage types change -00:43 +72: test/migration_test.dart: migrations sqlite additive changes use ALTER without a table rebuild -00:43 +73: test/migration_test.dart: migrations sqlite reviewed table removal handles restrictive foreign keys atomically -00:43 +74: test/migration_test.dart: migrations sqlite broken migration chain is refused before DDL -00:43 +75: test/migration_test.dart: migrations sqlite catalog distinguishes nullable non-rowid primary keys -00:43 +76: test/migration_test.dart: migrations sqlite rebuild preserves unmanaged index trigger and view definitions -00:43 +77: test/migration_test.dart: migrations sqlite foreign key check rolls back changed parent keys without cascading data away -00:43 +78: test/migration_test.dart: migrations sqlite unmodeled CHECK constraints are refused without losing their definition -00:44 +79: test/migration_test.dart: migrations postgres explicit table and column renames preserve incoming references and data -00:44 +80: test/migration_test.dart: migrations postgres constraint changes resolve catalog names and retain rows -00:44 +81: test/migration_test.dart: migrations postgres failed NOT NULL rebuild restores data history and foreign key settings -00:44 +82: test/migration_test.dart: migrations postgres reviewed conversion expressions preserve values when storage types change -00:44 +83: test/migration_test.dart: migrations postgres additive changes use ALTER without a table rebuild -00:44 +84: test/migration_test.dart: migrations postgres reviewed table removal handles restrictive foreign keys atomically -00:44 +85: test/migration_test.dart: migrations postgres broken migration chain is refused before DDL -00:44 +86: test/migration_test.dart: diff requires explicit renames, casts and backfills -00:44 +87: test/migration_test.dart: (tearDownAll) -00:44 +87: loading test/many_to_many_test.dart -00:44 +87: test/many_to_many_test.dart: many-to-many sqlite two declared FKs and a composite primary key enforce association identity -00:44 +88: test/many_to_many_test.dart: many-to-many sqlite forward projection keeps payload, shared targets and per-user limits in two statements -00:44 +89: test/many_to_many_test.dart: many-to-many sqlite reverse projection applies tie-breaking and offset to each team without changing root pagination -00:44 +90: test/many_to_many_test.dart: many-to-many sqlite nested team rosters deduplicate shared team keys and batch by level -00:44 +91: test/many_to_many_test.dart: many-to-many sqlite association predicates and counts stay in one SQL statement -00:44 +92: test/many_to_many_test.dart: many-to-many sqlite limited parameter capacity chunks associations without changing per-parent limits -00:44 +93: test/many_to_many_test.dart: many-to-many sqlite transaction rollback restores payload changes and new endpoints after an invalid association -00:44 +94: test/many_to_many_test.dart: many-to-many sqlite composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:44 +95: test/many_to_many_test.dart: many-to-many sqlite watch tracks junction payload and opposite endpoint changes after commit -00:44 +96: test/many_to_many_test.dart: many-to-many postgres two declared FKs and a composite primary key enforce association identity -00:44 +97: test/many_to_many_test.dart: many-to-many postgres forward projection keeps payload, shared targets and per-user limits in two statements -00:44 +98: test/many_to_many_test.dart: many-to-many postgres reverse projection applies tie-breaking and offset to each team without changing root pagination -00:44 +99: test/many_to_many_test.dart: many-to-many postgres nested team rosters deduplicate shared team keys and batch by level -00:44 +100: test/many_to_many_test.dart: many-to-many postgres association predicates and counts stay in one SQL statement -00:44 +101: test/many_to_many_test.dart: many-to-many postgres limited parameter capacity chunks associations without changing per-parent limits -00:44 +102: test/many_to_many_test.dart: many-to-many postgres transaction rollback restores payload changes and new endpoints after an invalid association -00:44 +103: test/many_to_many_test.dart: many-to-many postgres composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:44 +104: test/many_to_many_test.dart: many-to-many postgres watch tracks junction payload and opposite endpoint changes after commit -00:45 +105: loading test/migration_target_test.dart -00:45 +105: test/migration_target_test.dart: history requires a single engine even when empty or SQL is portable -00:45 +106: test/migration_target_test.dart: unselected expressions cannot change a saved plan, hash or source -00:45 +107: test/migration_target_test.dart: SQLite virtual indexes and computed mode changes ignore PostgreSQL limits -00:45 +108: test/migration_target_test.dart: replacing stored values with computed values requires explicit review -00:45 +109: test/migration_target_test.dart: older PostgreSQL is rejected before locks, journals or partial execution -00:45 +110: test/migration_target_test.dart: registry fixes its engine at initialization and refuses retargeting -00:45 +111: test/migration_target_test.dart: wrong engine fails before any migration SQL for runner and every connected CLI command -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -00:45 +112: test/migration_target_test.dart: PostgreSQL applies only its own generated conversion and rejects a SQLite history before SQL -00:45 +113: loading test/stream_test.dart -00:45 +113: test/stream_test.dart: sqlite cancellation between batch chunks prevents a partial commit -00:45 +114: test/stream_test.dart: sqlite cancellation interrupts an active cursor fetch -00:45 +115: test/stream_test.dart: sqlite timed-out mutations return no partial write -00:45 +116: test/stream_test.dart: sqlite database close stops a paused cursor and releases the driver -00:45 +117: test/stream_test.dart: sqlite cancellation between stream creation and listening needs no rollback -00:45 +118: test/stream_test.dart: sqlite bounded cursor fetches preserve parameters and typed rows -00:45 +119: test/stream_test.dart: sqlite pause stops fetches and early cancellation releases the lease -00:45 +120: test/stream_test.dart: sqlite await-for provides demand and take closes without exhausting -00:45 +121: test/stream_test.dart: sqlite relation projections load once per root batch -00:45 +122: test/stream_test.dart: sqlite mapper failure closes the cursor and rolls back -00:45 +123: test/stream_test.dart: sqlite early stream end inside a transaction preserves other work -00:45 +124: test/stream_test.dart: sqlite escaping paused stream cannot keep a transaction alive -00:45 +125: test/stream_test.dart: sqlite cancels executing SQL and allows the next statement -00:45 +126: test/stream_test.dart: sqlite statement timeout stops SQL and leaves a usable connection -00:45 +127: test/stream_test.dart: sqlite cancelled statement poisons a transaction even if caught -00:45 +128: test/stream_test.dart: sqlite cancelling a paused stream releases its connection before resume -00:45 +129: test/stream_test.dart: sqlite cancellation before starting performs no SQL -00:45 +130: test/stream_test.dart: postgres cancellation between batch chunks prevents a partial commit -00:45 +131: test/stream_test.dart: postgres cancellation interrupts an active cursor fetch -00:46 +132: test/stream_test.dart: postgres timed-out mutations return no partial write -00:46 +133: test/stream_test.dart: postgres database close stops a paused cursor and releases the driver -00:46 +134: test/stream_test.dart: postgres cancellation between stream creation and listening needs no rollback -00:46 +135: test/stream_test.dart: postgres bounded cursor fetches preserve parameters and typed rows -00:46 +136: test/stream_test.dart: postgres pause stops fetches and early cancellation releases the lease -00:46 +137: test/stream_test.dart: postgres await-for provides demand and take closes without exhausting -00:46 +138: test/stream_test.dart: postgres relation projections load once per root batch -00:46 +139: test/stream_test.dart: postgres mapper failure closes the cursor and rolls back -00:46 +140: test/stream_test.dart: postgres early stream end inside a transaction preserves other work -00:46 +141: test/stream_test.dart: postgres escaping paused stream cannot keep a transaction alive -00:46 +142: test/stream_test.dart: postgres cancels executing SQL and allows the next statement -00:46 +143: test/stream_test.dart: postgres statement timeout stops SQL and leaves a usable connection -00:46 +144: test/stream_test.dart: postgres cancelled statement poisons a transaction even if caught -00:46 +145: test/stream_test.dart: postgres cancelling a paused stream releases its connection before resume -00:46 +146: test/stream_test.dart: postgres cancellation before starting performs no SQL -00:46 +147: test/stream_test.dart: late PostgreSQL cancellation cannot reach the next statement -00:46 +148: test/stream_test.dart: borrowed pool requires cancellation support and retains ownership -00:46 +149: test/stream_test.dart: failed cancellation control discards the physical connection -00:46 +150: loading test/migration_recovery_test.dart -00:47 +150: test/migration_recovery_test.dart: (setUpAll) -00:47 +150: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:47 +151: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:56 +152: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:57 +153: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:57 +154: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:57 +155: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:57 +156: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:57 +157: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:57 +158: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:57 +159: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:57 +160: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:57 +161: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -01:00 +162: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -01:02 +163: test/migration_recovery_test.dart: (tearDownAll) -01:02 +163: loading test/acquisition_test.dart -01:02 +163: test/acquisition_test.dart: an expired lease cannot enter before a delayed Timer gets its turn -01:02 +164: test/acquisition_test.dart: acquisition sqlite expired raw writes never execute after a late lease becomes available -01:02 +165: test/acquisition_test.dart: acquisition sqlite query cancellation returns while the pool is still occupied -01:02 +166: test/acquisition_test.dart: acquisition sqlite typed mutations and batch transactions propagate acquisition limits -01:02 +167: test/acquisition_test.dart: acquisition sqlite expired transactions and sessions never enter their callbacks -01:02 +168: test/acquisition_test.dart: acquisition sqlite acquisition timeout stops on entry and statement timeout starts afterward -01:03 +169: test/acquisition_test.dart: acquisition sqlite invalid and already-cancelled acquisition options never queue work -01:03 +170: test/acquisition_test.dart: acquisition sqlite stream cancellation and timeout return without opening a cursor -01:03 +171: test/acquisition_test.dart: acquisition sqlite query watch acquisition failure can recover on a later invalidation -01:03 +172: test/acquisition_test.dart: acquisition sqlite close drains abandoned reservations without running their callbacks -01:03 +173: test/acquisition_test.dart: acquisition sqlite abandoned requests preserve pool reuse and subsequent action errors -01:03 +174: test/acquisition_test.dart: PostgreSQL native pool timeout is classified before entering the callback -01:03 +175: test/acquisition_test.dart: PostgreSQL connection establishment timeout does not cap pool waiting -01:03 +176: test/acquisition_test.dart: acquisition postgres expired raw writes never execute after a late lease becomes available -01:03 +177: test/acquisition_test.dart: acquisition postgres query cancellation returns while the pool is still occupied -01:03 +178: test/acquisition_test.dart: acquisition postgres typed mutations and batch transactions propagate acquisition limits -01:03 +179: test/acquisition_test.dart: acquisition postgres expired transactions and sessions never enter their callbacks -01:04 +180: test/acquisition_test.dart: acquisition postgres acquisition timeout stops on entry and statement timeout starts afterward -01:04 +181: test/acquisition_test.dart: acquisition postgres invalid and already-cancelled acquisition options never queue work -01:04 +182: test/acquisition_test.dart: acquisition postgres stream cancellation and timeout return without opening a cursor -01:04 +183: test/acquisition_test.dart: acquisition postgres query watch acquisition failure can recover on a later invalidation -01:04 +184: test/acquisition_test.dart: acquisition postgres close drains abandoned reservations without running their callbacks -01:04 +185: test/acquisition_test.dart: acquisition postgres abandoned requests preserve pool reuse and subsequent action errors -01:04 +186: test/acquisition_test.dart: PostgreSQL timeout during real connection initialization releases the late lease -01:04 +187: loading test/computed_test.dart -01:04 +187: test/computed_test.dart: computed sqlite native expressions preserve quoted identifiers, comments and SQL-like string values -01:04 +188: test/computed_test.dart: computed sqlite generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:04 +189: test/computed_test.dart: computed sqlite catalog/import roundtrip preserves SQL and modes and detects drift -01:07 +190: test/computed_test.dart: computed sqlite ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:07 +191: test/computed_test.dart: computed sqlite ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:07 +192: test/computed_test.dart: computed sqlite stored to ordinary materialization retains values and permits later writes -01:07 +193: test/computed_test.dart: computed sqlite explicit renamed expressions preserve indexes, checks, references and views -01:07 +194: test/computed_test.dart: computed sqlite invalid recomputation rolls back the table and migration history -01:07 +195: test/computed_test.dart: computed sqlite decimal computed SQL keeps precision coercion and imports exactly once -01:08 +196: test/computed_test.dart: computed postgres native expressions preserve quoted identifiers, comments and SQL-like string values -01:08 +197: test/computed_test.dart: computed postgres generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:08 +198: test/computed_test.dart: computed postgres catalog/import roundtrip preserves SQL and modes and detects drift -01:08 +199: test/computed_test.dart: computed postgres ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:08 +200: test/computed_test.dart: computed postgres ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:08 +201: test/computed_test.dart: computed postgres stored to ordinary materialization retains values and permits later writes -01:08 +202: test/computed_test.dart: computed postgres explicit renamed expressions preserve indexes, checks, references and views -01:09 +203: test/computed_test.dart: computed postgres invalid recomputation rolls back the table and migration history -01:09 +204: test/computed_test.dart: computed postgres decimal computed SQL keeps precision coercion and imports exactly once -01:09 +205: test/computed_test.dart: mode conversions need explicit plans and historical copy/backfill cannot write computed columns -01:09 +206: loading test/named_sql_test.dart -01:09 +206: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -01:09 +207: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -01:09 +208: test/named_sql_test.dart: generated client is deterministic and checked against its Dart declaration -01:12 +209: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -01:12 +210: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -01:12 +211: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -01:12 +212: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -01:12 +213: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -01:12 +214: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -01:12 +215: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -01:12 +216: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -01:12 +217: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -01:12 +218: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -01:12 +219: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -01:13 +220: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -01:13 +221: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -01:13 +222: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -01:13 +223: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -01:13 +224: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -01:13 +225: loading test/named_sql_generation_test.dart -01:13 +225: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -01:19 +226: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -01:21 +227: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -01:23 +228: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -01:23 +229: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -01:45 +230: loading test/decimal_average_test.dart -01:45 +230: test/decimal_average_test.dart: integer fractions round once and only bound their final result -01:46 +231: test/decimal_average_test.dart: decimal average sqlite empty null and constant inputs retain aggregate semantics -01:46 +232: test/decimal_average_test.dart: decimal average sqlite all rounding modes match exact signed sums across scales -01:46 +233: test/decimal_average_test.dart: decimal average sqlite valid means survive sums exceeding the finite numeric range -01:48 +234: test/decimal_average_test.dart: decimal average sqlite minimum fractions and outermost rounding scales stay exact -01:48 +235: test/decimal_average_test.dart: decimal average sqlite grouping HAVING ordering and CTE references preserve the mean -01:48 +236: test/decimal_average_test.dart: decimal average sqlite illegal window nesting and aggregate assignments fail before SQL -01:48 +237: test/decimal_average_test.dart: decimal average sqlite windows and rounded window means respect partitions and pagination -01:48 +238: test/decimal_average_test.dart: decimal average sqlite correlated means can update values and failures roll back transactions -01:48 +239: test/decimal_average_test.dart: decimal average sqlite batched relationship windows keep per-parent limits -01:48 +240: test/decimal_average_test.dart: decimal average sqlite sliding windows remove values, reset empty frames and reject varying policies -01:48 +241: test/decimal_average_test.dart: decimal average sqlite window means of grouped sums run after HAVING -01:48 +242: test/decimal_average_test.dart: decimal average postgres empty null and constant inputs retain aggregate semantics -01:48 +243: test/decimal_average_test.dart: decimal average postgres all rounding modes match exact signed sums across scales -01:48 +244: test/decimal_average_test.dart: decimal average postgres valid means survive sums exceeding the finite numeric range -01:48 +245: test/decimal_average_test.dart: decimal average postgres minimum fractions and outermost rounding scales stay exact -01:48 +246: test/decimal_average_test.dart: decimal average postgres grouping HAVING ordering and CTE references preserve the mean -01:48 +247: test/decimal_average_test.dart: decimal average postgres illegal window nesting and aggregate assignments fail before SQL -01:49 +248: test/decimal_average_test.dart: decimal average postgres windows and rounded window means respect partitions and pagination -01:49 +249: test/decimal_average_test.dart: decimal average postgres correlated means can update values and failures roll back transactions -01:49 +250: test/decimal_average_test.dart: decimal average postgres batched relationship windows keep per-parent limits -01:49 +251: test/decimal_average_test.dart: decimal average postgres window means of grouped sums run after HAVING -01:49 +252: test/decimal_average_test.dart: decimal average postgres volatile inputs and window ordering are evaluated once per row -01:49 +253: loading test/public_api_test.dart -01:49 +253: test/public_api_test.dart: generated clients expose row types and an immutable schema -01:49 +254: test/public_api_test.dart: entrypoint exports and runtime dependency boundaries stay explicit -01:56 +255: loading test/instant_test.dart -01:56 +255: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -01:56 +256: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -01:57 +257: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -01:57 +258: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -01:57 +259: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -01:57 +260: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -01:57 +261: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -02:00 +262: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -02:00 +263: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -02:00 +264: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -02:00 +265: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -02:00 +266: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -02:00 +267: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -02:00 +268: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -02:00 +269: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -02:00 +270: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -02:01 +271: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -02:01 +272: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -02:01 +273: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -02:01 +274: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -02:01 +275: loading test/codec_test.dart -02:01 +275: test/codec_test.dart: numeric aggregate text decodes without integer rounding or overflow -02:01 +276: test/codec_test.dart: erased codecs keep checked argument types without function getter casts -02:01 +277: test/codec_test.dart: mapped domain codecs preserve one decode and one encode boundary -02:01 +278: test/codec_test.dart: parsed JSON scalars and document presence have unambiguous decoding -02:01 +279: test/codec_test.dart: enum mappings reject duplicates and do not follow mutable caller maps -02:01 +280: loading test/sql_real_test.dart -02:01 +280: test/sql_real_test.dart: sqlite explicitly typed REAL parameters preserve floating storage -02:01 +281: test/sql_real_test.dart: postgres explicitly typed REAL parameters preserve floating storage -02:01 +282: loading test/migration_source_test.dart -02:01 +282: test/migration_source_test.dart: Dart emission preserves storage metadata and every operation without application imports -02:04 +283: test/migration_source_test.dart: project CLI creates Dart history, upgrades a database and preserves recorded fingerprints -03:29 +284: test/migration_source_test.dart: AOT migration bundle works without Dart sources or migration assets -03:37 +285: loading test/generator_test.dart -03:37 +285: test/generator_test.dart: (setUpAll) -03:37 +285: test/generator_test.dart: target capabilities are checked when choosing a migration engine -03:40 +286: test/generator_test.dart: unusable extension getters and generated symbol collisions fail at generation -03:42 +287: test/generator_test.dart: client and snapshot paths cannot overwrite the declaration -03:42 +288: test/generator_test.dart: record annotations, physical names, relations and snapshots -03:42 +289: test/generator_test.dart: output is deterministic and matches the committed generated client -03:47 +290: test/generator_test.dart: composite keys and more than six fields generate analyzable code -03:48 +291: test/generator_test.dart: computed keys are rejected instead of executing a fake row -03:48 +292: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -03:50 +293: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -03:53 +294: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty SQL and duplicate names -03:54 +295: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -03:55 +296: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -03:56 +297: test/generator_test.dart: invalid custom declaration: wrong_codec -03:56 +298: test/generator_test.dart: invalid custom declaration: nullable_codec -03:56 +299: test/generator_test.dart: invalid custom declaration: erased_extension -03:57 +300: test/generator_test.dart: invalid custom declaration: unknown_storage -03:57 +301: test/generator_test.dart: invalid custom declaration: private_codec -03:57 +302: test/generator_test.dart: invalid custom declaration: private_type -03:58 +303: test/generator_test.dart: invalid custom declaration: inline_codec -03:58 +304: test/generator_test.dart: invalid custom declaration: duplicate_codec -03:58 +305: test/generator_test.dart: invalid custom declaration: missing_codec -03:58 +306: test/generator_test.dart: invalid custom declaration: duplicate_labels -03:59 +307: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -03:59 +308: test/generator_test.dart: foreign keys require unique targets -03:59 +309: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -04:00 +310: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -04:00 +311: test/generator_test.dart: invalid query-only relation: lookup_computed -04:00 +312: test/generator_test.dart: invalid query-only relation: lookup_codec -04:00 +313: test/generator_test.dart: invalid query-only relation: lookup_inverse -04:01 +314: test/generator_test.dart: invalid query-only relation: lookup_type -04:01 +315: test/generator_test.dart: invalid query-only relation: lookup_delete -04:01 +316: test/generator_test.dart: Dart type errors stop generation -04:01 +317: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -04:02 +318: test/generator_test.dart: (tearDownAll) -04:02 +318: loading test/transaction_test.dart -04:02 +318: test/transaction_test.dart: transaction control sqlite idle callback expires, rolls back and cannot issue late SQL -04:02 +319: test/transaction_test.dart: transaction control sqlite deadline interrupts active SQL before releasing and reusing the lease -04:02 +320: test/transaction_test.dart: transaction control sqlite interrupted write rolls back and leaves the connection usable -04:02 +321: test/transaction_test.dart: transaction control sqlite explicit transaction cancellation stops an idle callback and active SQL -04:02 +322: test/transaction_test.dart: transaction control sqlite global transaction cancellation also abandons acquisition -04:02 +323: test/transaction_test.dart: transaction control sqlite savepoint inherits deadline, drains and rolls back through raw cleanup -04:03 +324: test/transaction_test.dart: transaction control sqlite deadline closes a paused transactional cursor before rollback -04:03 +325: test/transaction_test.dart: transaction control sqlite deadline interrupts an active transactional cursor fetch -04:03 +326: test/transaction_test.dart: transaction control sqlite transaction clock starts after acquisition and a borrowed session recovers -04:03 +327: test/transaction_test.dart: transaction control sqlite failed rollback is explicit and discards an uncertain connection -04:03 +328: test/transaction_test.dart: transaction control sqlite transaction controls reject adapters without actual cancellation -04:03 +329: test/transaction_test.dart: transaction control sqlite shorter statement limit retains its error and rolls back the transaction -04:03 +330: test/transaction_test.dart: transaction control sqlite monotonic deadline prevents commit after a CPU-bound callback -04:03 +331: test/transaction_test.dart: transaction control sqlite completed scopes remove their timers and cancellation listeners -04:03 +332: test/transaction_test.dart: transaction control sqlite confirmed commit wins a cancellation race; lost acknowledgement stays unknown -04:03 +333: test/transaction_test.dart: transaction control sqlite deferred constraint rejection at COMMIT is a known failure -04:03 +334: test/transaction_test.dart: transaction control sqlite invalid deadlines and already cancelled tokens never begin a transaction -04:03 +335: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=false -04:03 +336: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=true -04:03 +337: test/transaction_test.dart: transaction control postgres idle callback expires, rolls back and cannot issue late SQL -04:04 +338: test/transaction_test.dart: transaction control postgres deadline interrupts active SQL before releasing and reusing the lease -04:04 +339: test/transaction_test.dart: transaction control postgres interrupted write rolls back and leaves the connection usable -04:04 +340: test/transaction_test.dart: transaction control postgres explicit transaction cancellation stops an idle callback and active SQL -04:04 +341: test/transaction_test.dart: transaction control postgres global transaction cancellation also abandons acquisition -04:04 +342: test/transaction_test.dart: transaction control postgres savepoint inherits deadline, drains and rolls back through raw cleanup -04:04 +343: test/transaction_test.dart: transaction control postgres deadline closes a paused transactional cursor before rollback -04:04 +344: test/transaction_test.dart: transaction control postgres deadline interrupts an active transactional cursor fetch -04:04 +345: test/transaction_test.dart: transaction control postgres transaction clock starts after acquisition and a borrowed session recovers -04:05 +346: test/transaction_test.dart: transaction control postgres failed rollback is explicit and discards an uncertain connection -04:05 +347: test/transaction_test.dart: transaction control postgres transaction controls reject adapters without actual cancellation -04:05 +348: test/transaction_test.dart: transaction control postgres shorter statement limit retains its error and rolls back the transaction -04:05 +349: test/transaction_test.dart: transaction control postgres monotonic deadline prevents commit after a CPU-bound callback -04:05 +350: test/transaction_test.dart: transaction control postgres completed scopes remove their timers and cancellation listeners -04:05 +351: test/transaction_test.dart: transaction control postgres confirmed commit wins a cancellation race; lost acknowledgement stays unknown -04:05 +352: test/transaction_test.dart: transaction control postgres deferred constraint rejection at COMMIT is a known failure -04:05 +353: test/transaction_test.dart: transaction control postgres invalid deadlines and already cancelled tokens never begin a transaction -04:05 +354: test/transaction_test.dart: transaction control postgres SQLSTATE 40003 at COMMIT is never classified as a confirmed rejection -04:05 +355: test/transaction_test.dart: transaction control postgres real PostgreSQL serialization failure is classified after rollback -04:05 +356: test/transaction_test.dart: transaction control postgres real PostgreSQL deadlock rolls back exactly one transaction -04:06 +357: test/transaction_test.dart: SQLite COMMIT busy is a known rejection followed by verified rollback -04:06 +358: test/transaction_test.dart: SQLite deadline while BEGIN waits for a writer preserves its connection -04:06 +359: loading test/backfill_keys_test.dart -04:07 +359: test/backfill_keys_test.dart: backfill keys sqlite integer cursors preserve native values across resume -04:07 +360: test/backfill_keys_test.dart: backfill keys sqlite bigint cursors preserve native values across resume -04:07 +361: test/backfill_keys_test.dart: backfill keys sqlite text cursors preserve native values across resume -04:07 +362: test/backfill_keys_test.dart: backfill keys sqlite real cursors preserve native values across resume -04:07 +363: test/backfill_keys_test.dart: backfill keys sqlite boolean cursors preserve native values across resume -04:07 +364: test/backfill_keys_test.dart: backfill keys sqlite timestamp cursors preserve native values across resume -04:07 +365: test/backfill_keys_test.dart: backfill keys sqlite blob cursors preserve native values across resume -04:07 +366: test/backfill_keys_test.dart: backfill keys sqlite composite key batches account for every bound parameter -04:07 +367: test/backfill_keys_test.dart: backfill keys postgres integer cursors preserve native values across resume -04:07 +368: test/backfill_keys_test.dart: backfill keys postgres bigint cursors preserve native values across resume -04:07 +369: test/backfill_keys_test.dart: backfill keys postgres text cursors preserve native values across resume -04:07 +370: test/backfill_keys_test.dart: backfill keys postgres real cursors preserve native values across resume -04:07 +371: test/backfill_keys_test.dart: backfill keys postgres boolean cursors preserve native values across resume -04:07 +372: test/backfill_keys_test.dart: backfill keys postgres timestamp cursors preserve native values across resume -04:07 +373: test/backfill_keys_test.dart: backfill keys postgres blob cursors preserve native values across resume -04:07 +374: test/backfill_keys_test.dart: backfill keys postgres composite key batches account for every bound parameter -04:07 +375: test/backfill_keys_test.dart: invalid key, assignments and completion declarations fail before execution -04:07 +376: loading test/retry_test.dart -04:07 +376: test/retry_test.dart: retry sqlite opt-in retries roll back all writes and use new closed-over views -04:08 +377: test/retry_test.dart: retry sqlite default is one attempt; an explicit attempt limit is exact -04:08 +378: test/retry_test.dart: retry sqlite application and constraint errors and caught statement failures do not replay -04:08 +379: test/retry_test.dart: retry sqlite unknown commit and unconfirmed rollback prohibit replay -04:08 +380: test/retry_test.dart: retry sqlite retry time budget bounds active SQL and does not retry cancellation -04:08 +381: test/retry_test.dart: retry sqlite deadline and cancellation interrupt backoff without another callback -04:08 +382: test/retry_test.dart: retry sqlite time budget includes acquisition and abandons late callbacks -04:08 +383: test/retry_test.dart: retry sqlite transaction timeout is shared across attempts and sessions recover after expiry -04:08 +384: test/retry_test.dart: retry sqlite subscriptions publish only the final committed retry result -04:08 +385: test/retry_test.dart: retry sqlite invalid policies reject before SQL -04:08 +386: test/retry_test.dart: retry postgres opt-in retries roll back all writes and use new closed-over views -04:08 +387: test/retry_test.dart: retry postgres default is one attempt; an explicit attempt limit is exact -04:08 +388: test/retry_test.dart: retry postgres application and constraint errors and caught statement failures do not replay -04:08 +389: test/retry_test.dart: retry postgres unknown commit and unconfirmed rollback prohibit replay -04:08 +390: test/retry_test.dart: retry postgres retry time budget bounds active SQL and does not retry cancellation -04:08 +391: test/retry_test.dart: retry postgres deadline and cancellation interrupt backoff without another callback -04:08 +392: test/retry_test.dart: retry postgres time budget includes acquisition and abandons late callbacks -04:08 +393: test/retry_test.dart: retry postgres transaction timeout is shared across attempts and sessions recover after expiry -04:09 +394: test/retry_test.dart: retry postgres subscriptions publish only the final committed retry result -04:09 +395: test/retry_test.dart: retry postgres invalid policies reject before SQL -04:09 +396: test/retry_test.dart: retry postgres real unknown COMMIT response prohibits opted-in retry -04:09 +397: test/retry_test.dart: retry postgres real serialization conflict repeats the reads and decisions -04:09 +398: test/retry_test.dart: retry postgres real deadlock retries only the aborted callback -04:10 +399: test/retry_test.dart: SQLite WAL snapshot conflict replays reads after rollback -04:10 +400: test/retry_test.dart: SQLite real busy COMMIT: success -04:10 +401: test/retry_test.dart: SQLite real busy COMMIT: exhausted -04:10 +402: test/retry_test.dart: SQLite real busy COMMIT: deadline -04:10 +403: test/retry_test.dart: SQLite real busy COMMIT: cancel -04:10 +404: test/retry_test.dart: SQLite real busy COMMIT: shared-budget -04:10 +405: loading test/builder_test.dart -04:10 +405: test/builder_test.dart: build assets and standalone CLI produce identical client and snapshot -04:14 +406: test/builder_test.dart: multiple explicit schema roots resolve shared public types under lib -04:15 +407: test/builder_test.dart: invalid semantic_error never publishes partial outputs -04:15 +408: test/builder_test.dart: invalid enum_labels never publishes partial outputs -04:16 +409: test/builder_test.dart: invalid no_models never publishes partial outputs -04:17 +410: test/builder_test.dart: invalid part_file never publishes partial outputs -04:17 +411: test/builder_test.dart: unknown options fail instead of silently changing build behavior -04:18 +412: test/builder_test.dart: schema resolution can consume an earlier builder output -04:18 +413: loading test/temporal_precision_test.dart -04:19 +413: test/temporal_precision_test.dart: temporal precision sqlite generated writes, defaults and computed columns use database precision -04:19 +414: test/temporal_precision_test.dart: temporal precision sqlite SQL casts match all precisions on both sides of the PostgreSQL epoch -04:19 +415: test/temporal_precision_test.dart: temporal precision sqlite nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:19 +416: test/temporal_precision_test.dart: temporal precision sqlite rounded keys work in batch writes, upserts and foreign-key relation loading -04:19 +417: test/temporal_precision_test.dart: temporal precision sqlite catalog import preserves precision, defaults and computed expressions -04:21 +418: test/temporal_precision_test.dart: temporal precision sqlite narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:21 +419: test/temporal_precision_test.dart: temporal precision sqlite timestamp defaults and explicit six-digit catalogs retain precision -04:21 +420: test/temporal_precision_test.dart: temporal precision sqlite extended timestamps round in SQL without total-microsecond overflow -04:21 +421: test/temporal_precision_test.dart: temporal precision sqlite raw SQL respects native or managed precision constraints -04:21 +422: test/temporal_precision_test.dart: temporal precision postgres generated writes, defaults and computed columns use database precision -04:21 +423: test/temporal_precision_test.dart: temporal precision postgres SQL casts match all precisions on both sides of the PostgreSQL epoch -04:21 +424: test/temporal_precision_test.dart: temporal precision postgres nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:21 +425: test/temporal_precision_test.dart: temporal precision postgres rounded keys work in batch writes, upserts and foreign-key relation loading -04:21 +426: test/temporal_precision_test.dart: temporal precision postgres catalog import preserves precision, defaults and computed expressions -04:22 +427: test/temporal_precision_test.dart: temporal precision postgres narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:22 +428: test/temporal_precision_test.dart: temporal precision postgres timestamp defaults and explicit six-digit catalogs retain precision -04:22 +429: test/temporal_precision_test.dart: temporal precision postgres extended timestamps round in SQL without total-microsecond overflow -04:22 +430: test/temporal_precision_test.dart: temporal precision postgres raw SQL respects native or managed precision constraints -04:22 +431: test/temporal_precision_test.dart: value rounding keeps extended ranges and rejects overflow/invalid precision -04:22 +432: test/temporal_precision_test.dart: default and explicit six preserve historical snapshots and migration checksums -04:22 +433: test/temporal_precision_test.dart: generator rejects invalid annotations and preserves generated fixture metadata -04:25 +434: loading test/observation_test.dart -04:25 +434: test/observation_test.dart: observation sqlite actual lease queue and synchronous mapping have separate clocks -04:25 +435: test/observation_test.dart: observation sqlite expired acquisition emits one failure and no late SQL -04:25 +436: test/observation_test.dart: observation sqlite cancelled acquisition emits one failure and no late SQL -04:25 +437: test/observation_test.dart: observation sqlite sessions transactions and savepoints inherit hooks and report reused leases -04:25 +438: test/observation_test.dart: observation sqlite nested reads attribute decoding to each statement without repeating SQL -04:25 +439: test/observation_test.dart: observation sqlite SQL and mapper failures belong to their own phases -04:25 +440: test/observation_test.dart: observation sqlite cursor batches and returning writes produce decoding observations -04:25 +441: test/observation_test.dart: observation sqlite observer exceptions preserve successful commits and original mapper errors -04:25 +442: test/observation_test.dart: observation postgres actual lease queue and synchronous mapping have separate clocks -04:25 +443: test/observation_test.dart: observation postgres expired acquisition emits one failure and no late SQL -04:25 +444: test/observation_test.dart: observation postgres cancelled acquisition emits one failure and no late SQL -04:25 +445: test/observation_test.dart: observation postgres sessions transactions and savepoints inherit hooks and report reused leases -04:25 +446: test/observation_test.dart: observation postgres nested reads attribute decoding to each statement without repeating SQL -04:25 +447: test/observation_test.dart: observation postgres SQL and mapper failures belong to their own phases -04:25 +448: test/observation_test.dart: observation postgres cursor batches and returning writes produce decoding observations -04:25 +449: test/observation_test.dart: observation postgres observer exceptions preserve successful commits and original mapper errors -04:25 +450: test/observation_test.dart: synchronous driver acquisition failures are reported once -04:25 +451: loading test/integer_test.dart -04:26 +451: test/integer_test.dart: integer widths sqlite storage bounds, nulls and wider sums keep the normal integer value codec -04:26 +452: test/integer_test.dart: integer widths sqlite relations, union and cursors share int expressions across physical widths -04:26 +453: test/integer_test.dart: integer widths sqlite defaulted and quoted columns retain enforced nullable bounds -04:26 +454: test/integer_test.dart: integer widths sqlite Dart snapshots and imports retain widths without confusing value codecs -04:28 +455: test/integer_test.dart: integer widths sqlite widening and narrowing use reviewed conversions with atomic failure recovery -04:28 +456: test/integer_test.dart: integer widths sqlite column renames preserve width metadata and range constraints -04:28 +457: test/integer_test.dart: integer widths sqlite historical bounded backfills verify widths and resume normally -04:28 +458: test/integer_test.dart: integer widths sqlite changing an identity width preserves generation and rejects out-of-range explicit IDs -04:28 +459: test/integer_test.dart: integer widths sqlite quoted defaults and comments cannot masquerade as managed range checks -04:28 +460: test/integer_test.dart: integer widths sqlite extra weaker expressions remain unmanaged and cannot claim a narrower width -04:28 +461: test/integer_test.dart: integer widths postgres storage bounds, nulls and wider sums keep the normal integer value codec -04:28 +462: test/integer_test.dart: integer widths postgres relations, union and cursors share int expressions across physical widths -04:28 +463: test/integer_test.dart: integer widths postgres defaulted and quoted columns retain enforced nullable bounds -04:28 +464: test/integer_test.dart: integer widths postgres Dart snapshots and imports retain widths without confusing value codecs -04:29 +465: test/integer_test.dart: integer widths postgres widening and narrowing use reviewed conversions with atomic failure recovery -04:29 +466: test/integer_test.dart: integer widths postgres column renames preserve width metadata and range constraints -04:29 +467: test/integer_test.dart: integer widths postgres historical bounded backfills verify widths and resume normally -04:29 +468: test/integer_test.dart: integer widths postgres changing an identity width preserves generation and rejects out-of-range explicit IDs -04:29 +469: test/integer_test.dart: analyzer validates width annotations and integer-backed domain codecs -04:31 +470: test/integer_test.dart: width declarations reject wrong kinds and ranges without changing default snapshots -04:31 +471: loading test/relation_test.dart -04:31 +471: test/relation_test.dart: sqlite automatic unique joins preserve root pagination in one statement -04:31 +472: test/relation_test.dart: sqlite all-null projections still distinguish a present row from absence -04:31 +473: test/relation_test.dart: sqlite explicit batch deduplicates composite keys and skips partial nulls -04:31 +474: test/relation_test.dart: sqlite composite chunks include filter and window parameters with nested joins -04:31 +475: test/relation_test.dart: sqlite nested self joins and two edges to the same table use distinct aliases -04:31 +476: test/relation_test.dart: sqlite batch collection below a joined optional parent loads on the same plan -04:31 +477: test/relation_test.dart: sqlite join refuses unproven cardinality while automatic batch checks it -04:31 +478: test/relation_test.dart: sqlite filters and to-one pagination do not drop root rows -04:31 +479: test/relation_test.dart: sqlite joined records survive CTE projection and cursor decoding -04:31 +480: test/relation_test.dart: sqlite required tests row presence independently of nullable selected values -04:31 +481: test/relation_test.dart: sqlite relation aggregates restore the alias used by a joined selection -04:31 +482: test/relation_test.dart: sqlite reusing an identical edge deduplicates its join -04:31 +483: test/relation_test.dart: sqlite different filters need fresh relation occurrences -04:31 +484: test/relation_test.dart: sqlite large composite batches avoid expression-depth failure -04:31 +485: test/relation_test.dart: postgres automatic unique joins preserve root pagination in one statement -04:31 +486: test/relation_test.dart: postgres all-null projections still distinguish a present row from absence -04:31 +487: test/relation_test.dart: postgres explicit batch deduplicates composite keys and skips partial nulls -04:31 +488: test/relation_test.dart: postgres composite chunks include filter and window parameters with nested joins -04:31 +489: test/relation_test.dart: postgres nested self joins and two edges to the same table use distinct aliases -04:31 +490: test/relation_test.dart: postgres batch collection below a joined optional parent loads on the same plan -04:31 +491: test/relation_test.dart: postgres join refuses unproven cardinality while automatic batch checks it -04:31 +492: test/relation_test.dart: postgres filters and to-one pagination do not drop root rows -04:31 +493: test/relation_test.dart: postgres joined records survive CTE projection and cursor decoding -04:31 +494: test/relation_test.dart: postgres required tests row presence independently of nullable selected values -04:31 +495: test/relation_test.dart: postgres relation aggregates restore the alias used by a joined selection -04:31 +496: test/relation_test.dart: postgres reusing an identical edge deduplicates its join -04:32 +497: test/relation_test.dart: postgres different filters need fresh relation occurrences -04:32 +498: test/relation_test.dart: postgres large composite batches avoid expression-depth failure -04:32 +499: loading test/schema_boundary_test.dart -04:32 +499: test/schema_boundary_test.dart: schema rejects malformed names, keys and local FK definitions offline -04:32 +500: test/schema_boundary_test.dart: identifier case is resolved for the selected database -04:32 +501: test/schema_boundary_test.dart: generated sqlite schema executes its native computed-key contract -04:34 +502: test/schema_boundary_test.dart: generated postgres schema executes its native computed-key contract -04:36 +503: loading test/generated_database_test.dart -04:36 +503: test/generated_database_test.dart: (setUpAll) -04:36 +503: test/generated_database_test.dart: generated sqlite generated client, DDL, relations, dates and patches work together -04:36 +504: test/generated_database_test.dart: generated sqlite planning is read-only and repeat migration is a no-op -04:36 +505: test/generated_database_test.dart: generated sqlite compiled physical snapshot and full managed catalog comparison -04:36 +506: test/generated_database_test.dart: generated sqlite baseline preserves existing rows and verifies keys before recording history -04:36 +507: test/generated_database_test.dart: generated sqlite full verification sees defaults and additional unique constraints -04:36 +508: test/generated_database_test.dart: generated sqlite history tampering and missing applied migrations fail -04:36 +509: test/generated_database_test.dart: generated sqlite failed DDL rolls back schema and history atomically -04:36 +510: test/generated_database_test.dart: generated sqlite actual column drift is detected independently of history -04:36 +511: test/generated_database_test.dart: generated sqlite transaction control cannot be hidden behind SQL comments -04:36 +512: test/generated_database_test.dart: generated sqlite concurrent migration callers serialize against shared history -04:36 +513: test/generated_database_test.dart: generated postgres generated client, DDL, relations, dates and patches work together -04:36 +514: test/generated_database_test.dart: generated postgres planning is read-only and repeat migration is a no-op -04:36 +515: test/generated_database_test.dart: generated postgres compiled physical snapshot and full managed catalog comparison -04:36 +516: test/generated_database_test.dart: generated postgres baseline preserves existing rows and verifies keys before recording history -04:36 +517: test/generated_database_test.dart: generated postgres full verification sees defaults and additional unique constraints -04:36 +518: test/generated_database_test.dart: generated postgres history tampering and missing applied migrations fail -04:36 +519: test/generated_database_test.dart: generated postgres failed DDL rolls back schema and history atomically -04:36 +520: test/generated_database_test.dart: generated postgres actual column drift is detected independently of history -04:36 +521: test/generated_database_test.dart: generated postgres transaction control cannot be hidden behind SQL comments -04:36 +522: test/generated_database_test.dart: generated postgres concurrent migration callers serialize against shared history -04:36 +523: test/generated_database_test.dart: (tearDownAll) -04:36 +523: loading test/import_test.dart -04:36 +523: test/import_test.dart: catalog import sqlite imports records, identities, defaults and constraints then baselines unchanged rows -04:42 +524: test/import_test.dart: catalog import sqlite normalizes reserved, colliding and hostile identifiers without changing physical names -04:43 +525: test/import_test.dart: catalog import sqlite composite and self relations preserve key order and unique indexes -04:44 +526: test/import_test.dart: catalog import sqlite unsupported storage excludes its table while computed SQL imports with a review note -04:44 +527: test/import_test.dart: catalog import sqlite read-only snapshot and selected scope reject temporary shadowing and active transactions -04:44 +528: test/import_test.dart: catalog import sqlite physical storage mapping does not infer domain types from data -04:45 +529: test/import_test.dart: catalog import sqlite views triggers and specialized indexes remain in the review report -04:45 +530: test/import_test.dart: catalog import sqlite nullable primary keys, virtual tables and their storage tables need explicit handling -04:45 +531: test/import_test.dart: catalog import sqlite foreign keys with coercible but different Dart types are reported -04:45 +532: test/import_test.dart: catalog import postgres imports records, identities, defaults and constraints then baselines unchanged rows -04:47 +533: test/import_test.dart: catalog import postgres normalizes reserved, colliding and hostile identifiers without changing physical names -04:48 +534: test/import_test.dart: catalog import postgres composite and self relations preserve key order and unique indexes -04:49 +535: test/import_test.dart: catalog import postgres unsupported storage excludes its table while computed SQL imports with a review note -04:49 +536: test/import_test.dart: catalog import postgres read-only snapshot and selected scope reject temporary shadowing and active transactions -04:49 +537: test/import_test.dart: catalog import postgres physical storage mapping does not infer domain types from data -04:49 +538: test/import_test.dart: catalog import postgres views triggers and specialized indexes remain in the review report -04:50 +539: test/import_test.dart: catalog import postgres policy reports retain roles, commands, mode and row-security state -04:50 +540: test/import_test.dart: catalog import postgres always identity and inherited tables are not flattened into ordinary writable models -04:50 +541: loading test/database_test.dart -04:50 +541: test/database_test.dart: sqlite typed CRUD, exact projection and parameter binding -04:50 +542: test/database_test.dart: sqlite record and DTO mapping run only when decoding -04:50 +543: test/database_test.dart: sqlite immutable filters, empty IN, null and pagination -04:50 +544: test/database_test.dart: sqlite cross-table identity and duplicate assignments rejected before IO -04:50 +545: test/database_test.dart: sqlite commit, rollback, savepoint recovery and escaped session -04:50 +546: test/database_test.dart: sqlite caught statement failures cannot masquerade as a commit -04:50 +547: test/database_test.dart: sqlite recoverable constraint errors stay within their savepoint -04:50 +548: test/database_test.dart: sqlite unawaited work is drained and rolled back -04:50 +549: test/database_test.dart: sqlite close waits for active operations and rejects further use -04:50 +550: test/database_test.dart: sqlite leased sessions retain one connection across transactions and expire -04:50 +551: test/database_test.dart: sqlite native upsert updates atomically using existing and incoming values -04:50 +552: test/database_test.dart: sqlite bulk inserts split at parameter limits and opt into returning -04:50 +553: test/database_test.dart: sqlite a failed later bulk chunk rolls back all previous chunks -04:50 +554: test/database_test.dart: sqlite bulk optional field shapes preserve defaults and explicit null -04:50 +555: test/database_test.dart: sqlite relations CTEs preserve SQL column identity through mapped projections -04:50 +556: test/database_test.dart: sqlite relations explicit aliases support multiple joins and self joins -04:50 +557: test/database_test.dart: sqlite relations left join distinguishes a missing row from an all-null selection -04:50 +558: test/database_test.dart: sqlite relations correlated scalar, IN and EXISTS use nested SQL scopes -04:50 +559: test/database_test.dart: sqlite relations grouping is checked and window functions preserve row cardinality -04:50 +560: test/database_test.dart: sqlite relations root pagination and each parent limit use two queries -04:50 +561: test/database_test.dart: sqlite relations empty roots skip child queries; empty lists remain typed lists -04:50 +562: test/database_test.dart: sqlite relations nested relation projections and optional all-null projection -04:50 +563: test/database_test.dart: sqlite relations correlated any, none, count and every with SQL nulls -04:50 +564: test/database_test.dart: sqlite relations relation parameters bind correctly in projection and filter -04:50 +565: test/database_test.dart: sqlite keyset pagination uses a unique tie breaker in mixed directions -04:50 +566: test/database_test.dart: sqlite keyset pagination handles null order and validates versioned tokens -04:50 +567: test/database_test.dart: postgres typed CRUD, exact projection and parameter binding -04:50 +568: test/database_test.dart: postgres record and DTO mapping run only when decoding -04:50 +569: test/database_test.dart: postgres immutable filters, empty IN, null and pagination -04:50 +570: test/database_test.dart: postgres cross-table identity and duplicate assignments rejected before IO -04:50 +571: test/database_test.dart: postgres commit, rollback, savepoint recovery and escaped session -04:50 +572: test/database_test.dart: postgres caught statement failures cannot masquerade as a commit -04:50 +573: test/database_test.dart: postgres recoverable constraint errors stay within their savepoint -04:50 +574: test/database_test.dart: postgres unawaited work is drained and rolled back -04:51 +575: test/database_test.dart: postgres close waits for active operations and rejects further use -04:51 +576: test/database_test.dart: postgres leased sessions retain one connection across transactions and expire -04:51 +577: test/database_test.dart: postgres native upsert updates atomically using existing and incoming values -04:51 +578: test/database_test.dart: postgres bulk inserts split at parameter limits and opt into returning -04:51 +579: test/database_test.dart: postgres a failed later bulk chunk rolls back all previous chunks -04:51 +580: test/database_test.dart: postgres bulk optional field shapes preserve defaults and explicit null -04:51 +581: test/database_test.dart: postgres relations CTEs preserve SQL column identity through mapped projections -04:51 +582: test/database_test.dart: postgres relations explicit aliases support multiple joins and self joins -04:51 +583: test/database_test.dart: postgres relations left join distinguishes a missing row from an all-null selection -04:51 +584: test/database_test.dart: postgres relations correlated scalar, IN and EXISTS use nested SQL scopes -04:51 +585: test/database_test.dart: postgres relations grouping is checked and window functions preserve row cardinality -04:51 +586: test/database_test.dart: postgres relations root pagination and each parent limit use two queries -04:51 +587: test/database_test.dart: postgres relations empty roots skip child queries; empty lists remain typed lists -04:51 +588: test/database_test.dart: postgres relations nested relation projections and optional all-null projection -04:51 +589: test/database_test.dart: postgres relations correlated any, none, count and every with SQL nulls -04:51 +590: test/database_test.dart: postgres relations relation parameters bind correctly in projection and filter -04:51 +591: test/database_test.dart: postgres keyset pagination uses a unique tie breaker in mixed directions -04:51 +592: test/database_test.dart: postgres keyset pagination handles null order and validates versioned tokens -04:51 +593: loading test/union_test.dart -04:51 +593: test/union_test.dart: union sqlite SQL duplicate and NULL semantics use one statement -04:51 +594: test/union_test.dart: union sqlite heterogeneous records map only after SQL union and final pagination -04:51 +595: test/union_test.dart: union sqlite operand limits, order, parameters and final filter stay scoped -04:51 +596: test/union_test.dart: union sqlite repeated SQL expressions preserve positional columns -04:51 +597: test/union_test.dart: union sqlite nested sets preserve association, CTE exports and streaming -04:51 +598: test/union_test.dart: union sqlite same CTE names in distinct operands stay in separate scopes -04:51 +599: test/union_test.dart: union sqlite SQL scalar, IN subqueries, aggregate and zero-row results compose -04:51 +600: test/union_test.dart: union sqlite grouped branches and window branches remain SQL queries -04:51 +601: test/union_test.dart: union sqlite bound constant projections retain Dart types -04:51 +602: test/union_test.dart: union sqlite Dart maps and relation selections are rejected before execution -04:51 +603: test/union_test.dart: union sqlite codecs and nullability cannot silently change across operands -04:51 +604: test/union_test.dart: union sqlite shared domain codecs work, and nullable wrappers retain decoding semantics -04:51 +605: test/union_test.dart: union sqlite export references and outer join nullability are guarded -04:51 +606: test/union_test.dart: union sqlite compound mutations and mixed sessions fail before SQL -04:51 +607: test/union_test.dart: union sqlite all record arities use typed SQL decoding -04:51 +608: test/union_test.dart: union sqlite subscription observes committed changes in both operands -04:51 +609: test/union_test.dart: union postgres SQL duplicate and NULL semantics use one statement -04:51 +610: test/union_test.dart: union postgres heterogeneous records map only after SQL union and final pagination -04:51 +611: test/union_test.dart: union postgres operand limits, order, parameters and final filter stay scoped -04:51 +612: test/union_test.dart: union postgres repeated SQL expressions preserve positional columns -04:51 +613: test/union_test.dart: union postgres nested sets preserve association, CTE exports and streaming -04:51 +614: test/union_test.dart: union postgres same CTE names in distinct operands stay in separate scopes -04:51 +615: test/union_test.dart: union postgres SQL scalar, IN subqueries, aggregate and zero-row results compose -04:51 +616: test/union_test.dart: union postgres grouped branches and window branches remain SQL queries -04:51 +617: test/union_test.dart: union postgres bound constant projections retain Dart types -04:51 +618: test/union_test.dart: union postgres Dart maps and relation selections are rejected before execution -04:51 +619: test/union_test.dart: union postgres codecs and nullability cannot silently change across operands -04:51 +620: test/union_test.dart: union postgres shared domain codecs work, and nullable wrappers retain decoding semantics -04:51 +621: test/union_test.dart: union postgres export references and outer join nullability are guarded -04:51 +622: test/union_test.dart: union postgres compound mutations and mixed sessions fail before SQL -04:51 +623: test/union_test.dart: union postgres all record arities use typed SQL decoding -04:51 +624: test/union_test.dart: union postgres subscription observes committed changes in both operands -04:51 +625: loading test/import_cli_test.dart -04:52 +625: test/import_cli_test.dart: CLI sqlite imports reviews generates and baselines existing data without replacing files -05:42 +626: test/import_cli_test.dart: CLI postgres imports reviews generates and baselines existing data without replacing files -06:26 +627: loading test/decimal_division_test.dart -06:26 +627: test/decimal_division_test.dart: decimal division sqlite signed modes and scales match exact integer arithmetic -06:26 +628: test/decimal_division_test.dart: decimal division sqlite exact defaults reject lost digits and zero divisors, NULL propagates -06:26 +629: test/decimal_division_test.dart: decimal division sqlite precision exceeds native division scale without double rounding -06:26 +630: test/decimal_division_test.dart: decimal division sqlite huge remainders avoid overflowing intermediate scaled products -06:49 +631: test/decimal_division_test.dart: decimal division sqlite outermost integer scale and result overflow are explicit -06:50 +632: test/decimal_division_test.dart: decimal division sqlite aggregates windows CTEs predicates and unions compose -06:50 +633: test/decimal_division_test.dart: decimal division sqlite expression assignments and transaction rollback preserve values -06:50 +634: test/decimal_division_test.dart: decimal division sqlite window filtering ordering DISTINCT and pagination keep their SQL order -06:50 +635: test/decimal_division_test.dart: decimal division sqlite windows over grouped aggregates run after HAVING -06:50 +636: test/decimal_division_test.dart: decimal division sqlite window results compose through CTEs sets and correlated subqueries -06:50 +637: test/decimal_division_test.dart: decimal division sqlite batched relations retain per-parent window pagination -06:50 +638: test/decimal_division_test.dart: decimal division postgres signed modes and scales match exact integer arithmetic -06:50 +639: test/decimal_division_test.dart: decimal division postgres exact defaults reject lost digits and zero divisors, NULL propagates -06:50 +640: test/decimal_division_test.dart: decimal division postgres precision exceeds native division scale without double rounding -06:50 +641: test/decimal_division_test.dart: decimal division postgres huge remainders avoid overflowing intermediate scaled products -07:12 +642: test/decimal_division_test.dart: decimal division postgres outermost integer scale and result overflow are explicit -07:12 +643: test/decimal_division_test.dart: decimal division postgres aggregates windows CTEs predicates and unions compose -07:12 +644: test/decimal_division_test.dart: decimal division postgres expression assignments and transaction rollback preserve values -07:12 +645: test/decimal_division_test.dart: decimal division postgres window filtering ordering DISTINCT and pagination keep their SQL order -07:12 +646: test/decimal_division_test.dart: decimal division postgres windows over grouped aggregates run after HAVING -07:12 +647: test/decimal_division_test.dart: decimal division postgres window results compose through CTEs sets and correlated subqueries -07:12 +648: test/decimal_division_test.dart: decimal division postgres batched relations retain per-parent window pagination -07:12 +649: test/decimal_division_test.dart: decimal division postgres volatile operands are evaluated exactly once each -07:12 +650: loading test/query_boundary_test.dart -07:12 +650: test/query_boundary_test.dart: one failing cancellation listener cannot block other operations -07:12 +651: test/query_boundary_test.dart: sqlite explicit Backend construction rejects wrong transaction options before SQL -07:12 +652: test/query_boundary_test.dart: sqlite every close caller waits until the same pending lease is drained -07:12 +653: test/query_boundary_test.dart: sqlite subqueries and CTEs cannot escape their database or transaction -07:12 +654: test/query_boundary_test.dart: sqlite expired query cannot be embedded in its former parent -07:12 +655: test/query_boundary_test.dart: sqlite optional presence covers only its own left join -07:12 +656: test/query_boundary_test.dart: sqlite nested CTE decoding retains its nullable presence guards -07:12 +657: test/query_boundary_test.dart: sqlite mutation predicates and assignments reject aggregate/window SQL before execution -07:12 +658: test/query_boundary_test.dart: sqlite returning rejects empty selections and joined relationships -07:12 +659: test/query_boundary_test.dart: postgres explicit Backend construction rejects wrong transaction options before SQL -07:12 +660: test/query_boundary_test.dart: postgres every close caller waits until the same pending lease is drained -07:13 +661: test/query_boundary_test.dart: postgres subqueries and CTEs cannot escape their database or transaction -07:13 +662: test/query_boundary_test.dart: postgres expired query cannot be embedded in its former parent -07:13 +663: test/query_boundary_test.dart: postgres optional presence covers only its own left join -07:13 +664: test/query_boundary_test.dart: postgres nested CTE decoding retains its nullable presence guards -07:13 +665: test/query_boundary_test.dart: postgres mutation predicates and assignments reject aggregate/window SQL before execution -07:13 +666: test/query_boundary_test.dart: postgres returning rejects empty selections and joined relationships -07:13 +667: loading test/cli_test.dart -07:13 +667: test/cli_test.dart: Dart CLI generates, reviews, upgrades and verifies constraints without overwriting history -08:19 +668: test/cli_test.dart: Dart CLI baselines existing rows; read-only connections and invalid options reject writes -08:45 +669: test/cli_test.dart: Dart PostgreSQL configuration supports checked nontransactional recovery operations -09:31 +670: loading test/decimal_test.dart -09:31 +670: test/decimal_test.dart: exact decimal sqlite generated writes, defaults, patches and projections preserve all digits -09:31 +671: test/decimal_test.dart: exact decimal sqlite numeric ordering, predicates, IN, distinct and stable cursors -09:31 +672: test/decimal_test.dart: exact decimal sqlite sums, min/max, grouped aggregates and sliding windows stay exact -09:31 +673: test/decimal_test.dart: exact decimal sqlite computed CTEs, union results, subqueries and streaming keep decimal semantics -09:31 +674: test/decimal_test.dart: exact decimal sqlite numeric primary keys, foreign keys, joined and batched relations -09:31 +675: test/decimal_test.dart: exact decimal sqlite snapshots, catalog verification and imported declarations retain decimal storage -09:33 +676: test/decimal_test.dart: exact decimal sqlite aggregate intermediate overflow may cancel before a valid final result -09:33 +677: test/decimal_test.dart: exact decimal sqlite historical decimal keys resume backfills without lexical ordering or precision loss -09:33 +678: test/decimal_test.dart: exact decimal sqlite reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:33 +679: test/decimal_test.dart: exact decimal sqlite invalid external values fail decoding; later valid queries still work -09:33 +680: test/decimal_test.dart: exact decimal postgres generated writes, defaults, patches and projections preserve all digits -09:33 +681: test/decimal_test.dart: exact decimal postgres numeric ordering, predicates, IN, distinct and stable cursors -09:33 +682: test/decimal_test.dart: exact decimal postgres sums, min/max, grouped aggregates and sliding windows stay exact -09:33 +683: test/decimal_test.dart: exact decimal postgres computed CTEs, union results, subqueries and streaming keep decimal semantics -09:34 +684: test/decimal_test.dart: exact decimal postgres numeric primary keys, foreign keys, joined and batched relations -09:34 +685: test/decimal_test.dart: exact decimal postgres snapshots, catalog verification and imported declarations retain decimal storage -09:34 +686: test/decimal_test.dart: exact decimal postgres aggregate intermediate overflow may cancel before a valid final result -09:34 +687: test/decimal_test.dart: exact decimal postgres historical decimal keys resume backfills without lexical ordering or precision loss -09:34 +688: test/decimal_test.dart: exact decimal postgres reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:34 +689: test/decimal_test.dart: exact decimal postgres invalid external values fail decoding; later valid queries still work -09:34 +690: test/decimal_test.dart: decimal SQL rejects drivers without exact operations before execution -09:34 +691: test/decimal_test.dart: SQLite recognizes only column decimal collations, and detects drift -09:34 +692: loading test/watch_test.dart -09:35 +692: test/watch_test.dart: independent native SQLite workers require explicit notification -09:35 +693: test/watch_test.dart: watch sqlite lazy initial snapshot and relevant table invalidation -09:35 +694: test/watch_test.dart: watch sqlite transactions publish one snapshot after commit and none on rollback -09:35 +695: test/watch_test.dart: watch sqlite savepoint changes merge on release and disappear on rollback -09:35 +696: test/watch_test.dart: watch sqlite failed statements, zero-row writes and conflict no-ops do not refresh -09:35 +697: test/watch_test.dart: watch sqlite batch writes and upsert returning publish committed changes -09:35 +698: test/watch_test.dart: watch sqlite joined and nested batch relations track child writes -09:35 +699: test/watch_test.dart: watch sqlite relation predicates, SQL subqueries and CTE sources are dependencies -09:35 +700: test/watch_test.dart: watch sqlite declared cascade deletion invalidates a child-only query -09:35 +701: test/watch_test.dart: watch sqlite CASCADE tracks declared transitive deletion effects -09:35 +702: test/watch_test.dart: watch sqlite SET NULL tracks declared transitive deletion effects -09:35 +703: test/watch_test.dart: watch sqlite explicit JOINs and joined CTE definitions track both physical sources -09:35 +704: test/watch_test.dart: watch sqlite raw SQL declares changed and read tables; explicit invalidation follows transactions -09:35 +705: test/watch_test.dart: watch sqlite paused listeners coalesce writes and stop fetching until resumed -09:35 +706: test/watch_test.dart: watch sqlite invalidation during a read suppresses the stale snapshot and refetches -09:35 +707: test/watch_test.dart: watch sqlite decoding errors are reported and a later write can recover -09:35 +708: test/watch_test.dart: watch sqlite separate subscriptions and views of one driver receive changes -09:35 +709: test/watch_test.dart: watch sqlite watching leased sessions is rejected and cancelled tokens close the stream -09:35 +710: test/watch_test.dart: watch sqlite database close finishes watchers even when a listener is paused -09:35 +711: test/watch_test.dart: watch sqlite cancelling an active watched query interrupts SQL and releases its lease -09:35 +712: test/watch_test.dart: watch postgres lazy initial snapshot and relevant table invalidation -09:35 +713: test/watch_test.dart: watch postgres transactions publish one snapshot after commit and none on rollback -09:35 +714: test/watch_test.dart: watch postgres savepoint changes merge on release and disappear on rollback -09:35 +715: test/watch_test.dart: watch postgres failed statements, zero-row writes and conflict no-ops do not refresh -09:35 +716: test/watch_test.dart: watch postgres batch writes and upsert returning publish committed changes -09:35 +717: test/watch_test.dart: watch postgres joined and nested batch relations track child writes -09:35 +718: test/watch_test.dart: watch postgres relation predicates, SQL subqueries and CTE sources are dependencies -09:35 +719: test/watch_test.dart: watch postgres declared cascade deletion invalidates a child-only query -09:35 +720: test/watch_test.dart: watch postgres CASCADE tracks declared transitive deletion effects -09:35 +721: test/watch_test.dart: watch postgres SET NULL tracks declared transitive deletion effects -09:35 +722: test/watch_test.dart: watch postgres explicit JOINs and joined CTE definitions track both physical sources -09:35 +723: test/watch_test.dart: watch postgres raw SQL declares changed and read tables; explicit invalidation follows transactions -09:35 +724: test/watch_test.dart: watch postgres paused listeners coalesce writes and stop fetching until resumed -09:35 +725: test/watch_test.dart: watch postgres invalidation during a read suppresses the stale snapshot and refetches -09:35 +726: test/watch_test.dart: watch postgres decoding errors are reported and a later write can recover -09:35 +727: test/watch_test.dart: watch postgres separate subscriptions and views of one driver receive changes -09:35 +728: test/watch_test.dart: watch postgres watching leased sessions is rejected and cancelled tokens close the stream -09:35 +729: test/watch_test.dart: watch postgres database close finishes watchers even when a listener is paused -09:35 +730: test/watch_test.dart: watch postgres cancelling an active watched query interrupts SQL and releases its lease -09:35 +731: test/watch_test.dart: watch postgres lost commit acknowledgement triggers a conservative re-read -09:35 +732: test/watch_test.dart: watch postgres independent connections require an explicit external-write notification -09:36 +733: loading test/backfill_test.dart -09:36 +733: test/backfill_test.dart: backfill sqlite chunks pause and resume from a checksummed historical declaration -09:36 +734: test/backfill_test.dart: backfill sqlite failed chunk rolls back both writes and cursor, then resumes after repair -09:36 +735: test/backfill_test.dart: backfill sqlite failed completion proof preserves finished chunks without replay -09:36 +736: test/backfill_test.dart: backfill sqlite frozen upper bound and completion proof detect incompatible new writes -09:36 +737: test/backfill_test.dart: backfill sqlite competing runners share committed chunks without duplicate updates -09:36 +738: test/backfill_test.dart: backfill sqlite lost chunk commit acknowledgement preserves the committed frontier -09:36 +739: test/backfill_test.dart: backfill sqlite old checkpoint tables upgrade on apply; completed reads remain nonmutating -09:36 +740: test/backfill_test.dart: backfill sqlite completion conditions reject numeric results instead of truthiness -09:36 +741: test/backfill_test.dart: backfill sqlite empty input still requires the completion condition -09:36 +742: test/backfill_test.dart: backfill sqlite later ordinary migrations retain group atomicity after a completed backfill -09:36 +743: test/backfill_test.dart: backfill sqlite AFTER triggers cannot silently move keys past a committed cursor -09:36 +744: test/backfill_test.dart: backfill sqlite corrupt checkpoint dimensions and invalid run limits are rejected -09:36 +745: test/backfill_test.dart: backfill sqlite rebuild, chunked backfill and required-column rebuild preserve FK enforcement -09:36 +746: test/backfill_test.dart: backfill sqlite CLI previews non-atomic work and reports a bounded run as incomplete -09:58 +747: test/backfill_test.dart: backfill sqlite temporary tables cannot redirect the historical backfill target -09:58 +748: test/backfill_test.dart: backfill sqlite temporary objects cannot impersonate durable migration metadata -09:58 +749: test/backfill_test.dart: backfill sqlite committed chunks invalidate relevant typed query subscriptions -09:58 +750: test/backfill_test.dart: backfill sqlite real process exit after second chunk write resumes correctly -10:01 +751: test/backfill_test.dart: backfill sqlite real process exit after second chunk commit resumes correctly -10:04 +752: test/backfill_test.dart: backfill postgres chunks pause and resume from a checksummed historical declaration -10:04 +753: test/backfill_test.dart: backfill postgres failed chunk rolls back both writes and cursor, then resumes after repair -10:04 +754: test/backfill_test.dart: backfill postgres failed completion proof preserves finished chunks without replay -10:04 +755: test/backfill_test.dart: backfill postgres frozen upper bound and completion proof detect incompatible new writes -10:04 +756: test/backfill_test.dart: backfill postgres competing runners share committed chunks without duplicate updates -10:05 +757: test/backfill_test.dart: backfill postgres lost chunk commit acknowledgement preserves the committed frontier -10:05 +758: test/backfill_test.dart: backfill postgres old checkpoint tables upgrade on apply; completed reads remain nonmutating -10:05 +759: test/backfill_test.dart: backfill postgres completion conditions reject numeric results instead of truthiness -10:05 +760: test/backfill_test.dart: backfill postgres empty input still requires the completion condition -10:05 +761: test/backfill_test.dart: backfill postgres later ordinary migrations retain group atomicity after a completed backfill -10:05 +762: test/backfill_test.dart: backfill postgres AFTER triggers cannot silently move keys past a committed cursor -10:05 +763: test/backfill_test.dart: backfill postgres corrupt checkpoint dimensions and invalid run limits are rejected -10:05 +764: test/backfill_test.dart: backfill postgres CLI previews non-atomic work and reports a bounded run as incomplete -10:26 +765: test/backfill_test.dart: backfill postgres temporary tables cannot redirect the historical backfill target -10:26 +766: test/backfill_test.dart: backfill postgres temporary objects cannot impersonate durable migration metadata -10:26 +767: test/backfill_test.dart: backfill postgres row security cannot hide unfinished rows from the completion proof -10:26 +768: test/backfill_test.dart: backfill postgres inherited duplicate primary keys cannot advance a cursor -10:26 +769: test/backfill_test.dart: backfill postgres committed chunks invalidate relevant typed query subscriptions -10:26 +770: test/backfill_test.dart: backfill postgres real process exit after second chunk write resumes correctly -10:29 +771: test/backfill_test.dart: backfill postgres real process exit after second chunk commit resumes correctly -10:32 +772: loading test/custom_codec_test.dart -10:32 +772: test/custom_codec_test.dart: (setUpAll) -10:32 +772: test/custom_codec_test.dart: custom codecs sqlite generated custom values round trip through create, select and patch -10:32 +773: test/custom_codec_test.dart: custom codecs sqlite enum labels are stable text and unknown labels fail decoding -10:32 +774: test/custom_codec_test.dart: custom codecs sqlite domain validation and bound parameters apply to writes and predicates -10:32 +775: test/custom_codec_test.dart: custom codecs sqlite JSON documents distinguish SQL NULL, JSON null and string scalars -10:32 +776: test/custom_codec_test.dart: custom codecs sqlite nullable structured codec rejects JSON null instead of masking invalid data -10:32 +777: test/custom_codec_test.dart: custom codecs sqlite custom IDs and domain projections work across joined and batched relations -10:32 +778: test/custom_codec_test.dart: custom codecs sqlite custom values participate in atomic batch writes and native upsert -10:32 +779: test/custom_codec_test.dart: custom codecs sqlite custom IDs and enums survive cursor transport and streaming decoders -10:32 +780: test/custom_codec_test.dart: custom codecs sqlite migration snapshots retain physical storage without executing domain codecs -10:32 +781: test/custom_codec_test.dart: custom codecs postgres generated custom values round trip through create, select and patch -10:32 +782: test/custom_codec_test.dart: custom codecs postgres enum labels are stable text and unknown labels fail decoding -10:32 +783: test/custom_codec_test.dart: custom codecs postgres domain validation and bound parameters apply to writes and predicates -10:32 +784: test/custom_codec_test.dart: custom codecs postgres JSON documents distinguish SQL NULL, JSON null and string scalars -10:32 +785: test/custom_codec_test.dart: custom codecs postgres nullable structured codec rejects JSON null instead of masking invalid data -10:32 +786: test/custom_codec_test.dart: custom codecs postgres custom IDs and domain projections work across joined and batched relations -10:32 +787: test/custom_codec_test.dart: custom codecs postgres custom values participate in atomic batch writes and native upsert -10:32 +788: test/custom_codec_test.dart: custom codecs postgres custom IDs and enums survive cursor transport and streaming decoders -10:32 +789: test/custom_codec_test.dart: custom codecs postgres migration snapshots retain physical storage without executing domain codecs -10:32 +790: test/custom_codec_test.dart: (tearDownAll) -10:32 +790: loading test/plan_test.dart -10:32 +790: test/plan_test.dart: inspect sqlite reports composite key width and validates each potential batch -10:32 +791: test/plan_test.dart: inspect sqlite compiles nested plans without acquiring or decoding -10:32 +792: test/plan_test.dart: inspect sqlite distinguishes same-statement joins and skipped batches -10:32 +793: test/plan_test.dart: inspect sqlite retains CTE, UNION and raw SQL boundaries -10:32 +794: test/plan_test.dart: plan execution sqlite one-key SQL templates match executed SQL and internal slots -10:32 +795: test/plan_test.dart: inspect postgres reports composite key width and validates each potential batch -10:32 +796: test/plan_test.dart: inspect postgres compiles nested plans without acquiring or decoding -10:32 +797: test/plan_test.dart: inspect postgres distinguishes same-statement joins and skipped batches -10:32 +798: test/plan_test.dart: inspect postgres retains CTE, UNION and raw SQL boundaries -10:32 +799: test/plan_test.dart: plan execution postgres one-key SQL templates match executed SQL and internal slots -10:32 +800: loading test/check_test.dart -10:33 +800: test/check_test.dart: CHECK sqlite generated named and unnamed constraints enforce values and permit NULL -10:33 +801: test/check_test.dart: CHECK sqlite import preserves names and SQL then generates a matching snapshot -10:35 +802: test/check_test.dart: CHECK sqlite add, change and remove constraints validate old rows atomically -10:35 +803: test/check_test.dart: CHECK sqlite duplicate unnamed checks consume and drop exactly one catalog entry -10:35 +804: test/check_test.dart: CHECK sqlite explicit checked renames preserve rows, incoming references, indexes and views -10:35 +805: test/check_test.dart: CHECK sqlite verification preserves casts, precedence, literals and trailing comments -10:35 +806: test/check_test.dart: CHECK sqlite quoted Unicode identifiers retain SQLite case distinctions -10:35 +807: test/check_test.dart: CHECK sqlite undeclared CHECK is refused before a rebuild and remains enforced -10:35 +808: test/check_test.dart: CHECK postgres generated named and unnamed constraints enforce values and permit NULL -10:35 +809: test/check_test.dart: CHECK postgres import preserves names and SQL then generates a matching snapshot -10:36 +810: test/check_test.dart: CHECK postgres add, change and remove constraints validate old rows atomically -10:36 +811: test/check_test.dart: CHECK postgres duplicate unnamed checks consume and drop exactly one catalog entry -10:36 +812: test/check_test.dart: CHECK postgres explicit checked renames preserve rows, incoming references, indexes and views -10:36 +813: test/check_test.dart: CHECK postgres verification preserves casts, precedence, literals and trailing comments -10:36 +814: test/check_test.dart: CHECK postgres catalog excludes unvalidated and NO INHERIT constraints -10:36 +815: test/check_test.dart: CHECK postgres verification plans but does not execute volatile row expressions -10:36 +816: test/check_test.dart: dialect overrides only alter the affected backend and preserve old snapshot JSON -10:36 +817: loading test/temporal_test.dart -10:36 +817: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: false -10:36 +818: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: true -10:36 +819: test/temporal_test.dart: Gregorian cycles, era boundaries and full finite date range -10:36 +820: test/temporal_test.dart: local time precision, day endpoint and timestamp arithmetic -10:36 +821: test/temporal_test.dart: PostgreSQL binary registry covers boundaries without DateTime -10:36 +822: test/temporal_test.dart: local temporal sqlite generated writes, defaults, null patches and typed parameters -10:36 +823: test/temporal_test.dart: local temporal sqlite finite extremes and microseconds survive native rows and streams -10:36 +824: test/temporal_test.dart: local temporal sqlite calendar ordering, min/max, grouping and stable date cursors -10:36 +825: test/temporal_test.dart: local temporal sqlite equivalent date keys drive batched relations and unique indexes -10:36 +826: test/temporal_test.dart: local temporal sqlite time and local timestamp comparisons retain calendar semantics -10:36 +827: test/temporal_test.dart: local temporal sqlite reviewed text-to-time migration rolls back duplicate clock values -10:36 +828: test/temporal_test.dart: local temporal sqlite invalid external dates fail typed reads without damaging the connection -10:36 +829: test/temporal_test.dart: local temporal sqlite snapshots, catalog import and regenerated defaults agree -10:39 +830: test/temporal_test.dart: local temporal sqlite historical date keys resume a bounded backfill in calendar order -10:39 +831: test/temporal_test.dart: local temporal postgres catalog retains time-truncating casts in expression defaults -10:39 +832: test/temporal_test.dart: local temporal postgres generated writes, defaults, null patches and typed parameters -10:39 +833: test/temporal_test.dart: local temporal postgres finite extremes and microseconds survive native rows and streams -10:39 +834: test/temporal_test.dart: local temporal postgres calendar ordering, min/max, grouping and stable date cursors -10:39 +835: test/temporal_test.dart: local temporal postgres equivalent date keys drive batched relations and unique indexes -10:39 +836: test/temporal_test.dart: local temporal postgres time and local timestamp comparisons retain calendar semantics -10:39 +837: test/temporal_test.dart: local temporal postgres reviewed text-to-time migration rolls back duplicate clock values -10:39 +838: test/temporal_test.dart: local temporal postgres invalid external dates fail typed reads without damaging the connection -10:39 +839: test/temporal_test.dart: local temporal postgres snapshots, catalog import and regenerated defaults agree -10:40 +840: test/temporal_test.dart: local temporal postgres historical date keys resume a bounded backfill in calendar order -10:40 +841: loading test/unconstrained_relation_test.dart -10:40 +841: test/unconstrained_relation_test.dart: unconstrained sqlite catalogs contain no FK, implicit index or uniqueness promise -10:40 +842: test/unconstrained_relation_test.dart: unconstrained sqlite adding and removing a real FK remains a reviewed physical migration -10:40 +843: test/unconstrained_relation_test.dart: unconstrained sqlite unique joins preserve root rows, NULL components and missing targets -10:40 +844: test/unconstrained_relation_test.dart: unconstrained sqlite required target and filter visibility remain explicit -10:40 +845: test/unconstrained_relation_test.dart: unconstrained sqlite nonunique composite collections batch by tuple and page per parent -10:40 +846: test/unconstrained_relation_test.dart: unconstrained sqlite to-one uses cardinality checks without assuming a unique lookup -10:40 +847: test/unconstrained_relation_test.dart: unconstrained sqlite correlated filters and counts do not materialize related rows -10:40 +848: test/unconstrained_relation_test.dart: unconstrained sqlite self relations and inverse collections share the existing planner -10:40 +849: test/unconstrained_relation_test.dart: unconstrained sqlite delete has no cascade and watches track the joined target -10:40 +850: test/unconstrained_relation_test.dart: unconstrained sqlite transaction reads observe writes and outer rollback restores targets -10:40 +851: test/unconstrained_relation_test.dart: unconstrained sqlite streaming preserves optional and batched relation result shapes -10:40 +852: test/unconstrained_relation_test.dart: unconstrained sqlite floating relation keys retain storage intent and key equality -10:40 +853: test/unconstrained_relation_test.dart: unconstrained postgres catalogs contain no FK, implicit index or uniqueness promise -10:40 +854: test/unconstrained_relation_test.dart: unconstrained postgres adding and removing a real FK remains a reviewed physical migration -10:40 +855: test/unconstrained_relation_test.dart: unconstrained postgres unique joins preserve root rows, NULL components and missing targets -10:40 +856: test/unconstrained_relation_test.dart: unconstrained postgres required target and filter visibility remain explicit -10:40 +857: test/unconstrained_relation_test.dart: unconstrained postgres nonunique composite collections batch by tuple and page per parent -10:40 +858: test/unconstrained_relation_test.dart: unconstrained postgres to-one uses cardinality checks without assuming a unique lookup -10:40 +859: test/unconstrained_relation_test.dart: unconstrained postgres correlated filters and counts do not materialize related rows -10:40 +860: test/unconstrained_relation_test.dart: unconstrained postgres self relations and inverse collections share the existing planner -10:41 +861: test/unconstrained_relation_test.dart: unconstrained postgres delete has no cascade and watches track the joined target -10:41 +862: test/unconstrained_relation_test.dart: unconstrained postgres transaction reads observe writes and outer rollback restores targets -10:41 +863: test/unconstrained_relation_test.dart: unconstrained postgres streaming preserves optional and batched relation result shapes -10:41 +864: test/unconstrained_relation_test.dart: unconstrained postgres floating relation keys retain storage intent and key equality -10:41 +865: loading test/decimal_value_test.dart -10:41 +865: test/decimal_value_test.dart: normalization, scientific notation, equality and hash are exact -10:41 +866: test/decimal_value_test.dart: invalid and approximate inputs fail instead of silently rounding -10:41 +867: test/decimal_value_test.dart: range boundaries are checked without binary numeric conversions -10:41 +868: test/decimal_value_test.dart: addition, subtraction, multiplication and ordering keep every digit -10:41 +869: test/decimal_value_test.dart: division requires an explicit scale and rejects implicit rounding -10:41 +870: test/decimal_value_test.dart: rounding modes handle negative values, ties and negative scales -10:41 +871: test/decimal_value_test.dart: small fixed-scale arithmetic agrees with independent integer arithmetic -10:41 +872: All tests passed! diff --git a/research/validation/api-audit-usage.log b/research/validation/api-audit-usage.log deleted file mode 100644 index 4b8fb618..00000000 --- a/research/validation/api-audit-usage.log +++ /dev/null @@ -1,27 +0,0 @@ -dart run .dart_tool/audit_onboarding.dart -[seven@example.com] -Running build hooks...Running build hooks...dart run example/migrate.dart check -{ - "valid": true, - "dialect": "sqlite", - "migrations": [ - "0001_initial" - ] -} -Running build hooks...Running build hooks...dart run example/migrate.dart apply -{ - "applied": [ - "0001_initial" - ] -} -Running build hooks...Running build hooks...dart run example/migrate.dart verify -{ - "matches": true, - "differences": [], - "unmanaged": [] -} -Running build hooks...Running build hooks...dart run example/migrate.dart apply -{ - "applied": [] -} -Running build hooks...Running build hooks...README and migration lifecycle passed; final apply did not replay history. diff --git a/research/validation/api-audit.md b/research/validation/api-audit.md deleted file mode 100644 index 8daabed1..00000000 --- a/research/validation/api-audit.md +++ /dev/null @@ -1,79 +0,0 @@ -# API and correctness refactor validation - -Captured 2026-09-16 on macOS arm64. Dart 3.13.3 stable, sqlite3 package 3.6.0, -PostgreSQL 18.4 (Homebrew), and real headless Chrome 153. The PostgreSQL instance, -browser profiles and usage database were created for this run and cleaned up. - -Runtime source SHA-256: -`8344748e7f2dbc2e22a54f330510250c953e35ac67e7e92d238ec3dafb3d37f7`. -The digest covers lexically sorted Dart paths under `lib`/`bin` and -`pubspec.yaml`/`pubspec.lock`: UTF-8 path, NUL, file bytes, NUL. These files were -unchanged during the final native/browser/usage runs. Implementation commits are -`5b6feed` and `ef7438d`; final test harness, browser regression and documentation -changes are recorded in the subsequent local commit. - -| Check | Result | Evidence | -| --- | --- | --- | -| Complete native suite, PostgreSQL enabled | **872 passed, 10:41, exit 0** | [Full native log](api-audit-native.log) | -| Real Chrome JavaScript | **20 checks, passed, exit 0** | [JS log](api-audit-browser-js.log) | -| Real Chrome Dart WASM | **20 checks, passed, exit 0** | [WASM log](api-audit-browser-wasm.log) | -| README application example and persistent SQLite migration CLI | check/apply/verify/repeat apply passed; no replay | [Usage log](api-audit-usage.log) | -| Static analysis | No issues | `dart analyze` | -| Whitespace validation | Clean | `git diff --check` | - -The full native invocation includes existing regression suites plus 25 new test -cases: query/transaction boundaries, export/dependency contracts, schema/name -validation, both native computed-key contracts, and generation diagnostics. -It also runs real process-exit recovery, consumer source generation, build_runner -watch/recovery, historical fingerprints and an AOT migration bundle after deleting -the consumer's Dart sources and migration assets. - -Both browser runs add a new scenario for mixed transaction/root subqueries and -CTEs, independent left-join presence guards, valid nested optional decoding and -rejected writes with unchanged data. Their remaining scenarios cover the existing -generated API, codecs, relations, transactions, cursors, observation and OPFS -reload/recovery. The report independently records compilation mode and the pinned -SQLite WASM digest: -`13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4`. - -The README Dart block was executed directly, changing only its relative import -to reach the generated example client. It returned `[seven@example.com]` using -the generated `User` export. A fresh temporary file then passed the example -migration entrypoint's check/apply/verify/apply sequence; the second apply returned -an empty list. Historical definitions retained their recorded fingerprints. - -## Reproduction - -```sh -ORM_TEST_POSTGRES= dart test --reporter expanded -dart run tool/test_browser.dart -dart run tool/test_browser.dart --wasm -dart run example/migrate.dart check -ORM_SQLITE_PATH= dart run example/migrate.dart apply -ORM_SQLITE_PATH= dart run example/migrate.dart verify -ORM_SQLITE_PATH= dart run example/migrate.dart apply -dart analyze -git diff --check -``` - -Native runs use the standalone SDK and the installed CommandLineTools through -`DEVELOPER_DIR`. Run native tests and browser acceptance sequentially because -macOS native-asset startup can rewrite/code-sign a shared output. Disposable -consumer fixtures copy existing content-addressed SQLite downloads; sqlite3's -normal hook rechecks the published hash before reusing them. This avoids repeated -network downloads without replacing the native driver or AOT bundle path. - -## Limits - -This is the final clean full invocation, not a combination of earlier failing -and passing runs. An interrupted earlier attempt and the fixes it prompted are -described in [the audit](../api-correctness-audit.md). After the final native run, -only brace formatting in the test cache-copy helper and documentation/evidence -changed; no runtime, generator, schema or migration behavior changed. - -Android, Apple platforms and runtime/generation performance measurements were not -rerun. Their earlier captures stay tied to their recorded sources. Browser proof -is Chrome JS/WASM, not every browser. The native suite is SQLite and PostgreSQL -18.4, not every server version or custom build. Raw SQL, custom codecs/drivers and -unmanaged database objects retain their documented application-owned contracts. -No push or package publication was performed. diff --git a/research/validation/browser.json b/research/validation/browser.json deleted file mode 100644 index befb3d23..00000000 --- a/research/validation/browser.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "capturedAt": "2026-09-15T11:25:08.771057+00:00", - "runtimeSourceCommit": "85c4e42bf82924f806d1ce79ba96f1f61af61b1f", - "scope": "Real isolated Chrome JS and Dart WASM clients after switching generated schema snapshots and migration history to Dart; correctness checks, not throughput measurements.", - "commands": [ - "dart run tool/test_browser.dart", - "dart run tool/test_browser.dart --wasm" - ], - "reports": [ - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 64 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 41 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 16 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 19 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 34 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 26 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 3 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 2 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 4 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 4 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 159 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 96 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 3 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 22 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "js", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 55 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 39 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 14 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 29 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 22 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 161 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 29 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 102 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 39 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "wasm", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - } - ], - "sources": { - "tool/test_browser.dart": "f88fd14775583a1f91e8e96f912d37df3254bfd31c71b0f4afb68929ff38acfc", - "test/support/browser/main.dart": "c94b884c5756cc873f4a028224695556aee6030e7e2b16d64b2b9b80c3a4b58e", - "test/support/browser/schema.dart": "2435a02d5d25151f7c6b71bc204359b3a1a6161a09cc717fd471770d9e07aab7", - "test/support/browser/schema.snapshot.dart": "72f9a41c4dd3884df5f26eac0da77567ea45adecb01cd91d14fc8fb353de5241", - "lib/src/migrate/snapshot.dart": "7a8d38fdd9d9078deb97fab50e851462c52cf8df606c65be387ff49e9792dfc3", - "lib/src/migrate/source.dart": "ee9637f5efa3a85a89573561027bc82930ddac14e5578742cdf1a27f609ceb7e", - "lib/src/migrate/history.dart": "3d236ce64e8428e83692d2bce5f0b35a63401cbbba305d8a397bca402eb04563" - } -} diff --git a/research/validation/dart-migrations-aot.log b/research/validation/dart-migrations-aot.log deleted file mode 100644 index ecb55c0a..00000000 --- a/research/validation/dart-migrations-aot.log +++ /dev/null @@ -1,3 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/migration_source_test.dart -00:00 +0: AOT migration bundle works without Dart sources or migration assets -00:13 +1: All tests passed! diff --git a/research/validation/dart-migrations-backfill-aot.log b/research/validation/dart-migrations-backfill-aot.log deleted file mode 100644 index 27e42ec8..00000000 --- a/research/validation/dart-migrations-backfill-aot.log +++ /dev/null @@ -1,4 +0,0 @@ -Running build hooks...Running build hooks...Running link hooks...Running link hooks...Copying 1 build assets: -package:sqlite3/src/ffi/libsqlite3.g.dart -Generated: /Users/seven/workspace/dart-orm/.dart_tool/backfill-aot/bundle/bin/native_backfill -Native AOT: historical backfill declaration, bounded pause, concurrent resume and startup compatibility passed. diff --git a/research/validation/dart-migrations-followup.log b/research/validation/dart-migrations-followup.log deleted file mode 100644 index 9f9b8ab1..00000000 --- a/research/validation/dart-migrations-followup.log +++ /dev/null @@ -1,84 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/instant_test.dart -00:00 +0: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -00:00 +1: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -00:01 +2: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -00:01 +3: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -00:01 +4: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -00:01 +5: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -00:01 +6: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -00:03 +7: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -00:03 +8: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -00:03 +9: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -00:03 +10: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -00:03 +11: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -00:03 +12: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -00:04 +13: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -00:04 +14: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -00:04 +15: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -00:04 +16: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -00:04 +17: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -00:04 +18: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -00:04 +19: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -00:04 +20: loading test/generator_test.dart -00:05 +20: test/generator_test.dart: (setUpAll) -00:05 +20: test/generator_test.dart: client and snapshot paths cannot overwrite the declaration -00:05 +21: test/generator_test.dart: record annotations, physical names, relations and snapshots -00:06 +22: test/generator_test.dart: output is deterministic and matches the committed generated client -00:12 +23: test/generator_test.dart: composite keys and more than six fields generate analyzable code -00:13 +24: test/generator_test.dart: computed keys are rejected instead of executing a fake row -00:13 +25: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -00:16 +26: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -00:18 +27: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty overrides and duplicate names -00:20 +28: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -00:20 +29: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -00:21 +30: test/generator_test.dart: invalid custom declaration: wrong_codec -00:21 +31: test/generator_test.dart: invalid custom declaration: nullable_codec -00:22 +32: test/generator_test.dart: invalid custom declaration: erased_extension -00:22 +33: test/generator_test.dart: invalid custom declaration: unknown_storage -00:22 +34: test/generator_test.dart: invalid custom declaration: private_codec -00:22 +35: test/generator_test.dart: invalid custom declaration: private_type -00:23 +36: test/generator_test.dart: invalid custom declaration: inline_codec -00:23 +37: test/generator_test.dart: invalid custom declaration: duplicate_codec -00:23 +38: test/generator_test.dart: invalid custom declaration: missing_codec -00:23 +39: test/generator_test.dart: invalid custom declaration: duplicate_labels -00:24 +40: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -00:24 +41: test/generator_test.dart: foreign keys require unique targets -00:24 +42: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -00:25 +43: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -00:25 +44: test/generator_test.dart: invalid query-only relation: lookup_computed -00:25 +45: test/generator_test.dart: invalid query-only relation: lookup_codec -00:25 +46: test/generator_test.dart: invalid query-only relation: lookup_inverse -00:26 +47: test/generator_test.dart: invalid query-only relation: lookup_type -00:26 +48: test/generator_test.dart: invalid query-only relation: lookup_delete -00:26 +49: test/generator_test.dart: Dart type errors stop generation -00:26 +50: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -00:27 +51: test/generator_test.dart: (tearDownAll) -00:27 +51: loading test/named_sql_generation_test.dart -00:27 +51: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -00:31 +52: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -00:33 +53: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -00:35 +54: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -00:35 +55: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -00:55 +56: loading test/named_sql_test.dart -00:55 +56: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -00:55 +57: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -00:55 +58: test/named_sql_test.dart: generated client is deterministic and checked against its Dart declaration -00:58 +59: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -00:58 +60: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -00:58 +61: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -00:58 +62: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -00:58 +63: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -00:58 +64: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -00:58 +65: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -00:58 +66: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -00:58 +67: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -00:59 +68: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -00:59 +69: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -00:59 +70: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -00:59 +71: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -00:59 +72: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -00:59 +73: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -00:59 +74: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -00:59 +75: loading test/build_runner_test.dart -00:59 +75: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -01:33 +76: All tests passed! diff --git a/research/validation/dart-migrations-full.log b/research/validation/dart-migrations-full.log deleted file mode 100644 index 40ab5390..00000000 --- a/research/validation/dart-migrations-full.log +++ /dev/null @@ -1,903 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/client_default_test.dart -00:00 +0: test/client_default_test.dart: client defaults sqlite omitted values use typed Dart factories and return stored records -00:00 +1: test/client_default_test.dart: client defaults sqlite explicit values, null and SQL DEFAULT bypass their client factory -00:00 +2: test/client_default_test.dart: client defaults sqlite prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +3: test/client_default_test.dart: client defaults sqlite batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +4: test/client_default_test.dart: client defaults sqlite updates and conflict updates do not reset omitted client values -00:00 +5: test/client_default_test.dart: client defaults sqlite factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +6: test/client_default_test.dart: client defaults sqlite snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +7: test/client_default_test.dart: client defaults sqlite explicit DEFAULT can choose database identity instead of the client value -00:00 +8: test/client_default_test.dart: client defaults postgres omitted values use typed Dart factories and return stored records -00:00 +9: test/client_default_test.dart: client defaults postgres explicit values, null and SQL DEFAULT bypass their client factory -00:00 +10: test/client_default_test.dart: client defaults postgres prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +11: test/client_default_test.dart: client defaults postgres batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +12: test/client_default_test.dart: client defaults postgres updates and conflict updates do not reset omitted client values -00:00 +13: test/client_default_test.dart: client defaults postgres factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +14: test/client_default_test.dart: client defaults postgres snapshots, migrations, catalogs and imports never store or run a client factory -00:01 +15: test/client_default_test.dart: client defaults postgres explicit DEFAULT can choose database identity instead of the client value -00:01 +16: test/client_default_test.dart: generation reads factory references without executing them and supports moved output -00:02 +17: loading test/decimal_precision_test.dart -00:02 +17: test/decimal_precision_test.dart: decimal precision sqlite column coercion rounds signed ties and handles negative and excess scales -00:02 +18: test/decimal_precision_test.dart: decimal precision sqlite writes constrain columns while arithmetic and wider aggregates retain their result precision -00:02 +19: test/decimal_precision_test.dart: decimal precision sqlite batch inserts and upserts use column coercion and roll back as a unit -00:03 +20: test/decimal_precision_test.dart: decimal precision sqlite catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +21: test/decimal_precision_test.dart: decimal precision sqlite reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:05 +22: test/decimal_precision_test.dart: decimal precision sqlite physical renames and changing constrained defaults preserve column contracts -00:05 +23: test/decimal_precision_test.dart: decimal precision sqlite maximum declared precision and scale bounds remain exact -00:05 +24: test/decimal_precision_test.dart: decimal precision sqlite historical backfills retain constrained decimal key declarations -00:05 +25: test/decimal_precision_test.dart: decimal precision postgres column coercion rounds signed ties and handles negative and excess scales -00:05 +26: test/decimal_precision_test.dart: decimal precision postgres writes constrain columns while arithmetic and wider aggregates retain their result precision -00:05 +27: test/decimal_precision_test.dart: decimal precision postgres batch inserts and upserts use column coercion and roll back as a unit -00:05 +28: test/decimal_precision_test.dart: decimal precision postgres catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +29: test/decimal_precision_test.dart: decimal precision postgres reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:06 +30: test/decimal_precision_test.dart: decimal precision postgres physical renames and changing constrained defaults preserve column contracts -00:06 +31: test/decimal_precision_test.dart: decimal precision postgres maximum declared precision and scale bounds remain exact -00:06 +32: test/decimal_precision_test.dart: decimal precision postgres historical backfills retain constrained decimal key declarations -00:06 +33: test/decimal_precision_test.dart: SQLite enforces stored precision and checks default coercion and trusted-schema behavior -00:06 +34: test/decimal_precision_test.dart: generation validates DecimalDigits and retains custom decimal codec types -00:08 +35: test/decimal_precision_test.dart: metadata rejects invalid precisions scales and non-decimal columns -00:08 +36: loading test/selection_test.dart -00:08 +36: test/selection_test.dart: selection sqlite all typed arities defer mapping and preserve left-to-right evaluation -00:08 +37: test/selection_test.dart: selection sqlite first decoder failure prevents later fields and outer mapper -00:08 +38: test/selection_test.dart: selection sqlite dynamic fields snapshot keys and preserve nulls and mapping order -00:08 +39: test/selection_test.dart: selection postgres all typed arities defer mapping and preserve left-to-right evaluation -00:08 +40: test/selection_test.dart: selection postgres first decoder failure prevents later fields and outer mapper -00:08 +41: test/selection_test.dart: selection postgres dynamic fields snapshot keys and preserve nulls and mapping order -00:08 +42: loading test/build_runner_test.dart -00:08 +42: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -00:39 +43: loading test/schema_version_test.dart -00:39 +43: test/schema_version_test.dart: schema version sqlite invalid ranges fail before SQL; caller transactions stay usable -00:39 +44: test/schema_version_test.dart: schema version sqlite unversioned existing data is rejected without creating history -00:39 +45: test/schema_version_test.dart: schema version sqlite default requires latest; inclusive compatibility range is explicit -00:39 +46: test/schema_version_test.dart: schema version sqlite newer database rejects downgrade without changing data or schema -00:39 +47: test/schema_version_test.dart: schema version sqlite complete applied prefix is checked, including old checksums and missing entries -00:39 +48: test/schema_version_test.dart: schema version sqlite matching version does not claim that the catalog is free of drift -00:39 +49: test/schema_version_test.dart: schema version sqlite verified baseline is compatible without replaying creation -00:39 +50: test/schema_version_test.dart: schema version sqlite borrowed session uses a read snapshot and survives rejected versions -00:39 +51: test/schema_version_test.dart: schema version postgres invalid ranges fail before SQL; caller transactions stay usable -00:39 +52: test/schema_version_test.dart: schema version postgres unversioned existing data is rejected without creating history -00:39 +53: test/schema_version_test.dart: schema version postgres default requires latest; inclusive compatibility range is explicit -00:39 +54: test/schema_version_test.dart: schema version postgres newer database rejects downgrade without changing data or schema -00:39 +55: test/schema_version_test.dart: schema version postgres complete applied prefix is checked, including old checksums and missing entries -00:39 +56: test/schema_version_test.dart: schema version postgres matching version does not claim that the catalog is free of drift -00:39 +57: test/schema_version_test.dart: schema version postgres verified baseline is compatible without replaying creation -00:39 +58: test/schema_version_test.dart: schema version postgres borrowed session uses a read snapshot and survives rejected versions -00:39 +59: test/schema_version_test.dart: schema version postgres unfinished recoverable migration blocks startup until repaired and completed -00:39 +60: test/schema_version_test.dart: schema version postgres catalog and history share a snapshot and the supplied history is frozen -00:40 +61: test/schema_version_test.dart: SQLite version gate works on an explicitly read-only file -00:40 +62: loading test/types_test.dart -00:40 +62: test/types_test.dart: junction-table keys, enum payloads and nested results keep generated types -00:41 +63: test/types_test.dart: computed generated fields are statically readable and never writable -00:42 +64: test/types_test.dart: client-default create parameters preserve domain, nullable and timestamp types -00:43 +65: test/types_test.dart: query-only relation APIs retain result types and expose no graph writes -00:43 +66: test/types_test.dart: calendar APIs reject instants, strings and mixed temporal types -00:43 +67: test/types_test.dart: actual generated APIs reject wrong inputs, results and backend options -00:44 +68: loading test/migration_test.dart -00:44 +68: test/migration_test.dart: (setUpAll) -00:44 +68: test/migration_test.dart: migrations sqlite explicit table and column renames preserve incoming references and data -00:44 +69: test/migration_test.dart: migrations sqlite constraint changes resolve catalog names and retain rows -00:44 +70: test/migration_test.dart: migrations sqlite failed NOT NULL rebuild restores data history and foreign key settings -00:44 +71: test/migration_test.dart: migrations sqlite reviewed conversion expressions preserve values when storage types change -00:44 +72: test/migration_test.dart: migrations sqlite additive changes use ALTER without a table rebuild -00:44 +73: test/migration_test.dart: migrations sqlite reviewed table removal handles restrictive foreign keys atomically -00:44 +74: test/migration_test.dart: migrations sqlite broken migration chain is refused before DDL -00:44 +75: test/migration_test.dart: migrations sqlite catalog distinguishes nullable non-rowid primary keys -00:44 +76: test/migration_test.dart: migrations sqlite rebuild preserves unmanaged index trigger and view definitions -00:44 +77: test/migration_test.dart: migrations sqlite foreign key check rolls back changed parent keys without cascading data away -00:44 +78: test/migration_test.dart: migrations sqlite unmodeled CHECK constraints are refused without losing their definition -00:44 +79: test/migration_test.dart: migrations postgres explicit table and column renames preserve incoming references and data -00:44 +80: test/migration_test.dart: migrations postgres constraint changes resolve catalog names and retain rows -00:44 +81: test/migration_test.dart: migrations postgres failed NOT NULL rebuild restores data history and foreign key settings -00:44 +82: test/migration_test.dart: migrations postgres reviewed conversion expressions preserve values when storage types change -00:44 +83: test/migration_test.dart: migrations postgres additive changes use ALTER without a table rebuild -00:44 +84: test/migration_test.dart: migrations postgres reviewed table removal handles restrictive foreign keys atomically -00:44 +85: test/migration_test.dart: migrations postgres broken migration chain is refused before DDL -00:44 +86: test/migration_test.dart: diff requires explicit renames, casts and backfills -00:44 +87: test/migration_test.dart: (tearDownAll) -00:44 +87: loading test/many_to_many_test.dart -00:45 +87: test/many_to_many_test.dart: many-to-many sqlite two declared FKs and a composite primary key enforce association identity -00:45 +88: test/many_to_many_test.dart: many-to-many sqlite forward projection keeps payload, shared targets and per-user limits in two statements -00:45 +89: test/many_to_many_test.dart: many-to-many sqlite reverse projection applies tie-breaking and offset to each team without changing root pagination -00:45 +90: test/many_to_many_test.dart: many-to-many sqlite nested team rosters deduplicate shared team keys and batch by level -00:45 +91: test/many_to_many_test.dart: many-to-many sqlite association predicates and counts stay in one SQL statement -00:45 +92: test/many_to_many_test.dart: many-to-many sqlite limited parameter capacity chunks associations without changing per-parent limits -00:45 +93: test/many_to_many_test.dart: many-to-many sqlite transaction rollback restores payload changes and new endpoints after an invalid association -00:45 +94: test/many_to_many_test.dart: many-to-many sqlite composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:45 +95: test/many_to_many_test.dart: many-to-many sqlite watch tracks junction payload and opposite endpoint changes after commit -00:45 +96: test/many_to_many_test.dart: many-to-many postgres two declared FKs and a composite primary key enforce association identity -00:45 +97: test/many_to_many_test.dart: many-to-many postgres forward projection keeps payload, shared targets and per-user limits in two statements -00:45 +98: test/many_to_many_test.dart: many-to-many postgres reverse projection applies tie-breaking and offset to each team without changing root pagination -00:45 +99: test/many_to_many_test.dart: many-to-many postgres nested team rosters deduplicate shared team keys and batch by level -00:45 +100: test/many_to_many_test.dart: many-to-many postgres association predicates and counts stay in one SQL statement -00:45 +101: test/many_to_many_test.dart: many-to-many postgres limited parameter capacity chunks associations without changing per-parent limits -00:45 +102: test/many_to_many_test.dart: many-to-many postgres transaction rollback restores payload changes and new endpoints after an invalid association -00:45 +103: test/many_to_many_test.dart: many-to-many postgres composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:45 +104: test/many_to_many_test.dart: many-to-many postgres watch tracks junction payload and opposite endpoint changes after commit -00:45 +105: loading test/stream_test.dart -00:45 +105: test/stream_test.dart: sqlite cancellation between batch chunks prevents a partial commit -00:45 +106: test/stream_test.dart: sqlite cancellation interrupts an active cursor fetch -00:45 +107: test/stream_test.dart: sqlite timed-out mutations return no partial write -00:45 +108: test/stream_test.dart: sqlite database close stops a paused cursor and releases the driver -00:45 +109: test/stream_test.dart: sqlite cancellation between stream creation and listening needs no rollback -00:45 +110: test/stream_test.dart: sqlite bounded cursor fetches preserve parameters and typed rows -00:45 +111: test/stream_test.dart: sqlite pause stops fetches and early cancellation releases the lease -00:45 +112: test/stream_test.dart: sqlite await-for provides demand and take closes without exhausting -00:45 +113: test/stream_test.dart: sqlite relation projections load once per root batch -00:45 +114: test/stream_test.dart: sqlite mapper failure closes the cursor and rolls back -00:45 +115: test/stream_test.dart: sqlite early stream end inside a transaction preserves other work -00:45 +116: test/stream_test.dart: sqlite escaping paused stream cannot keep a transaction alive -00:45 +117: test/stream_test.dart: sqlite cancels executing SQL and allows the next statement -00:45 +118: test/stream_test.dart: sqlite statement timeout stops SQL and leaves a usable connection -00:46 +119: test/stream_test.dart: sqlite cancelled statement poisons a transaction even if caught -00:46 +120: test/stream_test.dart: sqlite cancelling a paused stream releases its connection before resume -00:46 +121: test/stream_test.dart: sqlite cancellation before starting performs no SQL -00:46 +122: test/stream_test.dart: postgres cancellation between batch chunks prevents a partial commit -00:46 +123: test/stream_test.dart: postgres cancellation interrupts an active cursor fetch -00:46 +124: test/stream_test.dart: postgres timed-out mutations return no partial write -00:46 +125: test/stream_test.dart: postgres database close stops a paused cursor and releases the driver -00:46 +126: test/stream_test.dart: postgres cancellation between stream creation and listening needs no rollback -00:46 +127: test/stream_test.dart: postgres bounded cursor fetches preserve parameters and typed rows -00:46 +128: test/stream_test.dart: postgres pause stops fetches and early cancellation releases the lease -00:46 +129: test/stream_test.dart: postgres await-for provides demand and take closes without exhausting -00:46 +130: test/stream_test.dart: postgres relation projections load once per root batch -00:46 +131: test/stream_test.dart: postgres mapper failure closes the cursor and rolls back -00:46 +132: test/stream_test.dart: postgres early stream end inside a transaction preserves other work -00:46 +133: test/stream_test.dart: postgres escaping paused stream cannot keep a transaction alive -00:46 +134: test/stream_test.dart: postgres cancels executing SQL and allows the next statement -00:46 +135: test/stream_test.dart: postgres statement timeout stops SQL and leaves a usable connection -00:46 +136: test/stream_test.dart: postgres cancelled statement poisons a transaction even if caught -00:46 +137: test/stream_test.dart: postgres cancelling a paused stream releases its connection before resume -00:46 +138: test/stream_test.dart: postgres cancellation before starting performs no SQL -00:46 +139: test/stream_test.dart: late PostgreSQL cancellation cannot reach the next statement -00:47 +140: test/stream_test.dart: borrowed pool requires cancellation support and retains ownership -00:47 +141: test/stream_test.dart: failed cancellation control discards the physical connection -00:47 +142: loading test/migration_recovery_test.dart -00:47 +142: test/migration_recovery_test.dart: (setUpAll) -00:47 +142: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:47 +143: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:57 +144: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:57 +145: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:57 +146: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:57 +147: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:57 +148: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:57 +149: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:57 +150: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:57 +151: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:57 +152: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:57 +153: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -01:03 +154: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -01:07 +155: test/migration_recovery_test.dart: (tearDownAll) -01:07 +155: loading test/acquisition_test.dart -01:07 +155: test/acquisition_test.dart: an expired lease cannot enter before a delayed Timer gets its turn -01:07 +156: test/acquisition_test.dart: acquisition sqlite expired raw writes never execute after a late lease becomes available -01:08 +157: test/acquisition_test.dart: acquisition sqlite query cancellation returns while the pool is still occupied -01:08 +158: test/acquisition_test.dart: acquisition sqlite typed mutations and batch transactions propagate acquisition limits -01:08 +159: test/acquisition_test.dart: acquisition sqlite expired transactions and sessions never enter their callbacks -01:08 +160: test/acquisition_test.dart: acquisition sqlite acquisition timeout stops on entry and statement timeout starts afterward -01:08 +161: test/acquisition_test.dart: acquisition sqlite invalid and already-cancelled acquisition options never queue work -01:08 +162: test/acquisition_test.dart: acquisition sqlite stream cancellation and timeout return without opening a cursor -01:08 +163: test/acquisition_test.dart: acquisition sqlite query watch acquisition failure can recover on a later invalidation -01:08 +164: test/acquisition_test.dart: acquisition sqlite close drains abandoned reservations without running their callbacks -01:08 +165: test/acquisition_test.dart: acquisition sqlite abandoned requests preserve pool reuse and subsequent action errors -01:08 +166: test/acquisition_test.dart: PostgreSQL native pool timeout is classified before entering the callback -01:08 +167: test/acquisition_test.dart: PostgreSQL connection establishment timeout does not cap pool waiting -01:09 +168: test/acquisition_test.dart: acquisition postgres expired raw writes never execute after a late lease becomes available -01:09 +169: test/acquisition_test.dart: acquisition postgres query cancellation returns while the pool is still occupied -01:09 +170: test/acquisition_test.dart: acquisition postgres typed mutations and batch transactions propagate acquisition limits -01:09 +171: test/acquisition_test.dart: acquisition postgres expired transactions and sessions never enter their callbacks -01:09 +172: test/acquisition_test.dart: acquisition postgres acquisition timeout stops on entry and statement timeout starts afterward -01:09 +173: test/acquisition_test.dart: acquisition postgres invalid and already-cancelled acquisition options never queue work -01:09 +174: test/acquisition_test.dart: acquisition postgres stream cancellation and timeout return without opening a cursor -01:09 +175: test/acquisition_test.dart: acquisition postgres query watch acquisition failure can recover on a later invalidation -01:09 +176: test/acquisition_test.dart: acquisition postgres close drains abandoned reservations without running their callbacks -01:09 +177: test/acquisition_test.dart: acquisition postgres abandoned requests preserve pool reuse and subsequent action errors -01:09 +178: test/acquisition_test.dart: PostgreSQL timeout during real connection initialization releases the late lease -01:09 +179: loading test/computed_test.dart -01:10 +179: test/computed_test.dart: computed sqlite native expressions preserve quoted identifiers, comments and SQL-like string values -01:10 +180: test/computed_test.dart: computed sqlite generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:10 +181: test/computed_test.dart: computed sqlite catalog/import roundtrip preserves SQL and modes and detects drift -01:12 +182: test/computed_test.dart: computed sqlite ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:12 +183: test/computed_test.dart: computed sqlite ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:12 +184: test/computed_test.dart: computed sqlite stored to ordinary materialization retains values and permits later writes -01:12 +185: test/computed_test.dart: computed sqlite explicit renamed expressions preserve indexes, checks, references and views -01:12 +186: test/computed_test.dart: computed sqlite invalid recomputation rolls back the table and migration history -01:12 +187: test/computed_test.dart: computed sqlite decimal computed SQL keeps precision coercion and imports exactly once -01:12 +188: test/computed_test.dart: computed postgres native expressions preserve quoted identifiers, comments and SQL-like string values -01:13 +189: test/computed_test.dart: computed postgres generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:13 +190: test/computed_test.dart: computed postgres catalog/import roundtrip preserves SQL and modes and detects drift -01:13 +191: test/computed_test.dart: computed postgres ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:13 +192: test/computed_test.dart: computed postgres ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:13 +193: test/computed_test.dart: computed postgres stored to ordinary materialization retains values and permits later writes -01:13 +194: test/computed_test.dart: computed postgres explicit renamed expressions preserve indexes, checks, references and views -01:13 +195: test/computed_test.dart: computed postgres invalid recomputation rolls back the table and migration history -01:13 +196: test/computed_test.dart: computed postgres decimal computed SQL keeps precision coercion and imports exactly once -01:14 +197: test/computed_test.dart: mode conversions need explicit plans and historical copy/backfill cannot write computed columns -01:14 +198: loading test/named_sql_test.dart -01:14 +198: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -01:14 +199: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -01:14 +200: test/named_sql_test.dart: generated client and manifest are deterministic and fresh -01:16 +201: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -01:16 +202: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -01:16 +203: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -01:16 +204: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -01:16 +205: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -01:16 +206: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -01:17 +207: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -01:17 +208: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -01:17 +209: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -01:17 +210: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -01:17 +211: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -01:17 +212: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -01:17 +213: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -01:17 +214: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -01:17 +215: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -01:17 +216: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -01:17 +217: loading test/named_sql_generation_test.dart -01:18 +217: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -01:21 +218: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -01:23 +219: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -01:24 +220: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -01:24 +221: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -01:44 +222: loading test/decimal_average_test.dart -01:44 +222: test/decimal_average_test.dart: integer fractions round once and only bound their final result -01:45 +223: test/decimal_average_test.dart: decimal average sqlite empty null and constant inputs retain aggregate semantics -01:46 +224: test/decimal_average_test.dart: decimal average sqlite all rounding modes match exact signed sums across scales -01:46 +225: test/decimal_average_test.dart: decimal average sqlite valid means survive sums exceeding the finite numeric range -01:48 +226: test/decimal_average_test.dart: decimal average sqlite minimum fractions and outermost rounding scales stay exact -01:48 +227: test/decimal_average_test.dart: decimal average sqlite grouping HAVING ordering and CTE references preserve the mean -01:48 +228: test/decimal_average_test.dart: decimal average sqlite illegal window nesting and aggregate assignments fail before SQL -01:48 +229: test/decimal_average_test.dart: decimal average sqlite windows and rounded window means respect partitions and pagination -01:48 +230: test/decimal_average_test.dart: decimal average sqlite correlated means can update values and failures roll back transactions -01:48 +231: test/decimal_average_test.dart: decimal average sqlite batched relationship windows keep per-parent limits -01:48 +232: test/decimal_average_test.dart: decimal average sqlite sliding windows remove values, reset empty frames and reject varying policies -01:48 +233: test/decimal_average_test.dart: decimal average sqlite window means of grouped sums run after HAVING -01:48 +234: test/decimal_average_test.dart: decimal average postgres empty null and constant inputs retain aggregate semantics -01:48 +235: test/decimal_average_test.dart: decimal average postgres all rounding modes match exact signed sums across scales -01:48 +236: test/decimal_average_test.dart: decimal average postgres valid means survive sums exceeding the finite numeric range -01:48 +237: test/decimal_average_test.dart: decimal average postgres minimum fractions and outermost rounding scales stay exact -01:48 +238: test/decimal_average_test.dart: decimal average postgres grouping HAVING ordering and CTE references preserve the mean -01:48 +239: test/decimal_average_test.dart: decimal average postgres illegal window nesting and aggregate assignments fail before SQL -01:48 +240: test/decimal_average_test.dart: decimal average postgres windows and rounded window means respect partitions and pagination -01:48 +241: test/decimal_average_test.dart: decimal average postgres correlated means can update values and failures roll back transactions -01:48 +242: test/decimal_average_test.dart: decimal average postgres batched relationship windows keep per-parent limits -01:48 +243: test/decimal_average_test.dart: decimal average postgres window means of grouped sums run after HAVING -01:48 +244: test/decimal_average_test.dart: decimal average postgres volatile inputs and window ordering are evaluated once per row -01:48 +245: loading test/instant_test.dart -01:48 +245: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -01:48 +246: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -01:50 +247: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -01:50 +247 -1: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable [E] - PathNotFoundException: Cannot open file, path = 'test/support/instants/0001_legacy.json' (OS Error: No such file or directory, errno = 2) - test/instant_test.dart 92:11 main. - -01:50 +247 -1: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -01:50 +248 -1: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -01:50 +249 -1: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -01:50 +250 -1: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -01:52 +251 -1: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -01:52 +252 -1: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -01:52 +253 -1: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -01:52 +254 -1: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -01:52 +255 -1: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -01:52 +256 -1: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -01:52 +257 -1: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -01:52 +258 -1: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -01:52 +259 -1: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -01:53 +260 -1: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -01:53 +261 -1: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -01:53 +262 -1: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -01:53 +263 -1: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -01:53 +264 -1: loading test/codec_test.dart -01:53 +264 -1: test/codec_test.dart: numeric aggregate text decodes without integer rounding or overflow -01:53 +265 -1: test/codec_test.dart: erased codecs keep checked argument types without function getter casts -01:53 +266 -1: test/codec_test.dart: mapped domain codecs preserve one decode and one encode boundary -01:53 +267 -1: test/codec_test.dart: parsed JSON scalars and document presence have unambiguous decoding -01:53 +268 -1: test/codec_test.dart: enum mappings reject duplicates and do not follow mutable caller maps -01:53 +269 -1: loading test/sql_real_test.dart -01:53 +269 -1: test/sql_real_test.dart: sqlite explicitly typed REAL parameters preserve floating storage -01:53 +270 -1: test/sql_real_test.dart: postgres explicitly typed REAL parameters preserve floating storage -01:53 +271 -1: loading test/migration_source_test.dart -01:53 +271 -1: test/migration_source_test.dart: Dart emission preserves storage metadata and every operation without application imports -01:58 +272 -1: test/migration_source_test.dart: project CLI creates Dart history, upgrades a database and preserves recorded fingerprints -03:17 +273 -1: test/migration_source_test.dart: AOT migration bundle works without Dart sources or migration assets -03:28 +274 -1: loading test/generator_test.dart -03:28 +274 -1: test/generator_test.dart: (setUpAll) -03:28 +274 -1: test/generator_test.dart: record annotations, physical names, relations and snapshots -03:30 +275 -1: test/generator_test.dart: output is deterministic and matches the committed generated client -03:35 +276 -1: test/generator_test.dart: composite keys and more than six fields generate analyzable code -03:36 +277 -1: test/generator_test.dart: computed keys are rejected instead of executing a fake row -03:36 +278 -1: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -03:39 +279 -1: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -03:41 +280 -1: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty overrides and duplicate names -03:43 +281 -1: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -03:43 +282 -1: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -03:44 +283 -1: test/generator_test.dart: invalid custom declaration: wrong_codec -03:44 +284 -1: test/generator_test.dart: invalid custom declaration: nullable_codec -03:44 +285 -1: test/generator_test.dart: invalid custom declaration: erased_extension -03:45 +286 -1: test/generator_test.dart: invalid custom declaration: unknown_storage -03:45 +287 -1: test/generator_test.dart: invalid custom declaration: private_codec -03:45 +288 -1: test/generator_test.dart: invalid custom declaration: private_type -03:45 +289 -1: test/generator_test.dart: invalid custom declaration: inline_codec -03:46 +290 -1: test/generator_test.dart: invalid custom declaration: duplicate_codec -03:46 +291 -1: test/generator_test.dart: invalid custom declaration: missing_codec -03:46 +292 -1: test/generator_test.dart: invalid custom declaration: duplicate_labels -03:46 +293 -1: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -03:47 +294 -1: test/generator_test.dart: foreign keys require unique targets -03:47 +295 -1: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -03:47 +296 -1: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -03:48 +297 -1: test/generator_test.dart: invalid query-only relation: lookup_computed -03:48 +298 -1: test/generator_test.dart: invalid query-only relation: lookup_codec -03:48 +299 -1: test/generator_test.dart: invalid query-only relation: lookup_inverse -03:48 +300 -1: test/generator_test.dart: invalid query-only relation: lookup_type -03:49 +301 -1: test/generator_test.dart: invalid query-only relation: lookup_delete -03:49 +302 -1: test/generator_test.dart: Dart type errors stop generation -03:49 +303 -1: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -03:50 +304 -1: test/generator_test.dart: (tearDownAll) -03:50 +304 -1: loading test/transaction_test.dart -03:50 +304 -1: test/transaction_test.dart: transaction control sqlite idle callback expires, rolls back and cannot issue late SQL -03:50 +305 -1: test/transaction_test.dart: transaction control sqlite deadline interrupts active SQL before releasing and reusing the lease -03:50 +306 -1: test/transaction_test.dart: transaction control sqlite interrupted write rolls back and leaves the connection usable -03:50 +307 -1: test/transaction_test.dart: transaction control sqlite explicit transaction cancellation stops an idle callback and active SQL -03:50 +308 -1: test/transaction_test.dart: transaction control sqlite global transaction cancellation also abandons acquisition -03:50 +309 -1: test/transaction_test.dart: transaction control sqlite savepoint inherits deadline, drains and rolls back through raw cleanup -03:50 +310 -1: test/transaction_test.dart: transaction control sqlite deadline closes a paused transactional cursor before rollback -03:50 +311 -1: test/transaction_test.dart: transaction control sqlite deadline interrupts an active transactional cursor fetch -03:51 +312 -1: test/transaction_test.dart: transaction control sqlite transaction clock starts after acquisition and a borrowed session recovers -03:51 +313 -1: test/transaction_test.dart: transaction control sqlite failed rollback is explicit and discards an uncertain connection -03:51 +314 -1: test/transaction_test.dart: transaction control sqlite transaction controls reject adapters without actual cancellation -03:51 +315 -1: test/transaction_test.dart: transaction control sqlite shorter statement limit retains its error and rolls back the transaction -03:51 +316 -1: test/transaction_test.dart: transaction control sqlite monotonic deadline prevents commit after a CPU-bound callback -03:51 +317 -1: test/transaction_test.dart: transaction control sqlite completed scopes remove their timers and cancellation listeners -03:51 +318 -1: test/transaction_test.dart: transaction control sqlite confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:51 +319 -1: test/transaction_test.dart: transaction control sqlite deferred constraint rejection at COMMIT is a known failure -03:51 +320 -1: test/transaction_test.dart: transaction control sqlite invalid deadlines and already cancelled tokens never begin a transaction -03:51 +321 -1: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=false -03:51 +322 -1: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=true -03:51 +323 -1: test/transaction_test.dart: transaction control postgres idle callback expires, rolls back and cannot issue late SQL -03:51 +324 -1: test/transaction_test.dart: transaction control postgres deadline interrupts active SQL before releasing and reusing the lease -03:51 +325 -1: test/transaction_test.dart: transaction control postgres interrupted write rolls back and leaves the connection usable -03:51 +326 -1: test/transaction_test.dart: transaction control postgres explicit transaction cancellation stops an idle callback and active SQL -03:52 +327 -1: test/transaction_test.dart: transaction control postgres global transaction cancellation also abandons acquisition -03:52 +328 -1: test/transaction_test.dart: transaction control postgres savepoint inherits deadline, drains and rolls back through raw cleanup -03:52 +329 -1: test/transaction_test.dart: transaction control postgres deadline closes a paused transactional cursor before rollback -03:52 +330 -1: test/transaction_test.dart: transaction control postgres deadline interrupts an active transactional cursor fetch -03:52 +331 -1: test/transaction_test.dart: transaction control postgres transaction clock starts after acquisition and a borrowed session recovers -03:52 +332 -1: test/transaction_test.dart: transaction control postgres failed rollback is explicit and discards an uncertain connection -03:52 +333 -1: test/transaction_test.dart: transaction control postgres transaction controls reject adapters without actual cancellation -03:52 +334 -1: test/transaction_test.dart: transaction control postgres shorter statement limit retains its error and rolls back the transaction -03:52 +335 -1: test/transaction_test.dart: transaction control postgres monotonic deadline prevents commit after a CPU-bound callback -03:52 +336 -1: test/transaction_test.dart: transaction control postgres completed scopes remove their timers and cancellation listeners -03:53 +337 -1: test/transaction_test.dart: transaction control postgres confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:53 +338 -1: test/transaction_test.dart: transaction control postgres deferred constraint rejection at COMMIT is a known failure -03:53 +339 -1: test/transaction_test.dart: transaction control postgres invalid deadlines and already cancelled tokens never begin a transaction -03:53 +340 -1: test/transaction_test.dart: transaction control postgres SQLSTATE 40003 at COMMIT is never classified as a confirmed rejection -03:53 +341 -1: test/transaction_test.dart: transaction control postgres real PostgreSQL serialization failure is classified after rollback -03:53 +342 -1: test/transaction_test.dart: transaction control postgres real PostgreSQL deadlock rolls back exactly one transaction -03:54 +343 -1: test/transaction_test.dart: SQLite COMMIT busy is a known rejection followed by verified rollback -03:54 +344 -1: test/transaction_test.dart: SQLite deadline while BEGIN waits for a writer preserves its connection -03:54 +345 -1: loading test/backfill_keys_test.dart -03:54 +345 -1: test/backfill_keys_test.dart: backfill keys sqlite integer cursors preserve native values across resume -03:54 +346 -1: test/backfill_keys_test.dart: backfill keys sqlite bigint cursors preserve native values across resume -03:54 +347 -1: test/backfill_keys_test.dart: backfill keys sqlite text cursors preserve native values across resume -03:54 +348 -1: test/backfill_keys_test.dart: backfill keys sqlite real cursors preserve native values across resume -03:54 +349 -1: test/backfill_keys_test.dart: backfill keys sqlite boolean cursors preserve native values across resume -03:54 +350 -1: test/backfill_keys_test.dart: backfill keys sqlite timestamp cursors preserve native values across resume -03:54 +351 -1: test/backfill_keys_test.dart: backfill keys sqlite blob cursors preserve native values across resume -03:54 +352 -1: test/backfill_keys_test.dart: backfill keys sqlite composite key batches account for every bound parameter -03:54 +353 -1: test/backfill_keys_test.dart: backfill keys postgres integer cursors preserve native values across resume -03:54 +354 -1: test/backfill_keys_test.dart: backfill keys postgres bigint cursors preserve native values across resume -03:55 +355 -1: test/backfill_keys_test.dart: backfill keys postgres text cursors preserve native values across resume -03:55 +356 -1: test/backfill_keys_test.dart: backfill keys postgres real cursors preserve native values across resume -03:55 +357 -1: test/backfill_keys_test.dart: backfill keys postgres boolean cursors preserve native values across resume -03:55 +358 -1: test/backfill_keys_test.dart: backfill keys postgres timestamp cursors preserve native values across resume -03:55 +359 -1: test/backfill_keys_test.dart: backfill keys postgres blob cursors preserve native values across resume -03:55 +360 -1: test/backfill_keys_test.dart: backfill keys postgres composite key batches account for every bound parameter -03:55 +361 -1: test/backfill_keys_test.dart: invalid key, assignments and completion declarations fail before execution -03:55 +362 -1: loading test/retry_test.dart -03:55 +362 -1: test/retry_test.dart: retry sqlite opt-in retries roll back all writes and use new closed-over views -03:55 +363 -1: test/retry_test.dart: retry sqlite default is one attempt; an explicit attempt limit is exact -03:55 +364 -1: test/retry_test.dart: retry sqlite application and constraint errors and caught statement failures do not replay -03:55 +365 -1: test/retry_test.dart: retry sqlite unknown commit and unconfirmed rollback prohibit replay -03:55 +366 -1: test/retry_test.dart: retry sqlite retry time budget bounds active SQL and does not retry cancellation -03:55 +367 -1: test/retry_test.dart: retry sqlite deadline and cancellation interrupt backoff without another callback -03:55 +368 -1: test/retry_test.dart: retry sqlite time budget includes acquisition and abandons late callbacks -03:55 +369 -1: test/retry_test.dart: retry sqlite transaction timeout is shared across attempts and sessions recover after expiry -03:55 +370 -1: test/retry_test.dart: retry sqlite subscriptions publish only the final committed retry result -03:55 +371 -1: test/retry_test.dart: retry sqlite invalid policies reject before SQL -03:55 +372 -1: test/retry_test.dart: retry postgres opt-in retries roll back all writes and use new closed-over views -03:56 +373 -1: test/retry_test.dart: retry postgres default is one attempt; an explicit attempt limit is exact -03:56 +374 -1: test/retry_test.dart: retry postgres application and constraint errors and caught statement failures do not replay -03:56 +375 -1: test/retry_test.dart: retry postgres unknown commit and unconfirmed rollback prohibit replay -03:56 +376 -1: test/retry_test.dart: retry postgres retry time budget bounds active SQL and does not retry cancellation -03:56 +377 -1: test/retry_test.dart: retry postgres deadline and cancellation interrupt backoff without another callback -03:56 +378 -1: test/retry_test.dart: retry postgres time budget includes acquisition and abandons late callbacks -03:56 +379 -1: test/retry_test.dart: retry postgres transaction timeout is shared across attempts and sessions recover after expiry -03:56 +380 -1: test/retry_test.dart: retry postgres subscriptions publish only the final committed retry result -03:56 +381 -1: test/retry_test.dart: retry postgres invalid policies reject before SQL -03:56 +382 -1: test/retry_test.dart: retry postgres real unknown COMMIT response prohibits opted-in retry -03:56 +383 -1: test/retry_test.dart: retry postgres real serialization conflict repeats the reads and decisions -03:56 +384 -1: test/retry_test.dart: retry postgres real deadlock retries only the aborted callback -03:57 +385 -1: test/retry_test.dart: SQLite WAL snapshot conflict replays reads after rollback -03:57 +386 -1: test/retry_test.dart: SQLite real busy COMMIT: success -03:57 +387 -1: test/retry_test.dart: SQLite real busy COMMIT: exhausted -03:57 +388 -1: test/retry_test.dart: SQLite real busy COMMIT: deadline -03:57 +389 -1: test/retry_test.dart: SQLite real busy COMMIT: cancel -03:57 +390 -1: test/retry_test.dart: SQLite real busy COMMIT: shared-budget -03:58 +391 -1: loading test/builder_test.dart -03:58 +391 -1: test/builder_test.dart: build assets and standalone CLI produce identical client and snapshot -04:01 +392 -1: test/builder_test.dart: multiple explicit schema roots resolve shared public types under lib -04:01 +393 -1: test/builder_test.dart: invalid semantic_error never publishes partial outputs -04:02 +394 -1: test/builder_test.dart: invalid enum_labels never publishes partial outputs -04:03 +395 -1: test/builder_test.dart: invalid no_models never publishes partial outputs -04:04 +396 -1: test/builder_test.dart: invalid part_file never publishes partial outputs -04:04 +397 -1: test/builder_test.dart: unknown options fail instead of silently changing build behavior -04:04 +398 -1: test/builder_test.dart: schema resolution can consume an earlier builder output -04:05 +399 -1: loading test/temporal_precision_test.dart -04:05 +399 -1: test/temporal_precision_test.dart: temporal precision sqlite generated writes, defaults and computed columns use database precision -04:05 +400 -1: test/temporal_precision_test.dart: temporal precision sqlite SQL casts match all precisions on both sides of the PostgreSQL epoch -04:05 +401 -1: test/temporal_precision_test.dart: temporal precision sqlite nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:05 +402 -1: test/temporal_precision_test.dart: temporal precision sqlite rounded keys work in batch writes, upserts and foreign-key relation loading -04:05 +403 -1: test/temporal_precision_test.dart: temporal precision sqlite catalog import preserves precision, defaults and computed expressions -04:08 +404 -1: test/temporal_precision_test.dart: temporal precision sqlite narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:08 +405 -1: test/temporal_precision_test.dart: temporal precision sqlite timestamp defaults and explicit six-digit catalogs retain precision -04:08 +406 -1: test/temporal_precision_test.dart: temporal precision sqlite extended timestamps round in SQL without total-microsecond overflow -04:08 +407 -1: test/temporal_precision_test.dart: temporal precision sqlite raw SQL respects native or managed precision constraints -04:08 +408 -1: test/temporal_precision_test.dart: temporal precision postgres generated writes, defaults and computed columns use database precision -04:08 +409 -1: test/temporal_precision_test.dart: temporal precision postgres SQL casts match all precisions on both sides of the PostgreSQL epoch -04:08 +410 -1: test/temporal_precision_test.dart: temporal precision postgres nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:08 +411 -1: test/temporal_precision_test.dart: temporal precision postgres rounded keys work in batch writes, upserts and foreign-key relation loading -04:08 +412 -1: test/temporal_precision_test.dart: temporal precision postgres catalog import preserves precision, defaults and computed expressions -04:08 +413 -1: test/temporal_precision_test.dart: temporal precision postgres narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:09 +414 -1: test/temporal_precision_test.dart: temporal precision postgres timestamp defaults and explicit six-digit catalogs retain precision -04:09 +415 -1: test/temporal_precision_test.dart: temporal precision postgres extended timestamps round in SQL without total-microsecond overflow -04:09 +416 -1: test/temporal_precision_test.dart: temporal precision postgres raw SQL respects native or managed precision constraints -04:09 +417 -1: test/temporal_precision_test.dart: value rounding keeps extended ranges and rejects overflow/invalid precision -04:09 +418 -1: test/temporal_precision_test.dart: default and explicit six preserve historical snapshots and migration checksums -04:09 +419 -1: test/temporal_precision_test.dart: generator rejects invalid annotations and preserves generated fixture metadata -04:11 +420 -1: loading test/observation_test.dart -04:11 +420 -1: test/observation_test.dart: observation sqlite actual lease queue and synchronous mapping have separate clocks -04:11 +421 -1: test/observation_test.dart: observation sqlite expired acquisition emits one failure and no late SQL -04:11 +422 -1: test/observation_test.dart: observation sqlite cancelled acquisition emits one failure and no late SQL -04:11 +423 -1: test/observation_test.dart: observation sqlite sessions transactions and savepoints inherit hooks and report reused leases -04:11 +424 -1: test/observation_test.dart: observation sqlite nested reads attribute decoding to each statement without repeating SQL -04:11 +425 -1: test/observation_test.dart: observation sqlite SQL and mapper failures belong to their own phases -04:11 +426 -1: test/observation_test.dart: observation sqlite cursor batches and returning writes produce decoding observations -04:11 +427 -1: test/observation_test.dart: observation sqlite observer exceptions preserve successful commits and original mapper errors -04:11 +428 -1: test/observation_test.dart: observation postgres actual lease queue and synchronous mapping have separate clocks -04:11 +429 -1: test/observation_test.dart: observation postgres expired acquisition emits one failure and no late SQL -04:11 +430 -1: test/observation_test.dart: observation postgres cancelled acquisition emits one failure and no late SQL -04:11 +431 -1: test/observation_test.dart: observation postgres sessions transactions and savepoints inherit hooks and report reused leases -04:12 +432 -1: test/observation_test.dart: observation postgres nested reads attribute decoding to each statement without repeating SQL -04:12 +433 -1: test/observation_test.dart: observation postgres SQL and mapper failures belong to their own phases -04:12 +434 -1: test/observation_test.dart: observation postgres cursor batches and returning writes produce decoding observations -04:12 +435 -1: test/observation_test.dart: observation postgres observer exceptions preserve successful commits and original mapper errors -04:12 +436 -1: test/observation_test.dart: synchronous driver acquisition failures are reported once -04:12 +437 -1: loading test/integer_test.dart -04:12 +437 -1: test/integer_test.dart: integer widths sqlite storage bounds, nulls and wider sums keep the normal integer value codec -04:12 +438 -1: test/integer_test.dart: integer widths sqlite relations, union and cursors share int expressions across physical widths -04:12 +439 -1: test/integer_test.dart: integer widths sqlite defaulted and quoted columns retain enforced nullable bounds -04:12 +440 -1: test/integer_test.dart: integer widths sqlite Dart snapshots and imports retain widths without confusing value codecs -04:14 +441 -1: test/integer_test.dart: integer widths sqlite widening and narrowing use reviewed conversions with atomic failure recovery -04:14 +442 -1: test/integer_test.dart: integer widths sqlite column renames preserve width metadata and range constraints -04:14 +443 -1: test/integer_test.dart: integer widths sqlite historical bounded backfills verify widths and resume normally -04:14 +444 -1: test/integer_test.dart: integer widths sqlite changing an identity width preserves generation and rejects out-of-range explicit IDs -04:14 +445 -1: test/integer_test.dart: integer widths sqlite quoted defaults and comments cannot masquerade as managed range checks -04:14 +446 -1: test/integer_test.dart: integer widths sqlite extra weaker expressions remain unmanaged and cannot claim a narrower width -04:14 +447 -1: test/integer_test.dart: integer widths postgres storage bounds, nulls and wider sums keep the normal integer value codec -04:14 +448 -1: test/integer_test.dart: integer widths postgres relations, union and cursors share int expressions across physical widths -04:14 +449 -1: test/integer_test.dart: integer widths postgres defaulted and quoted columns retain enforced nullable bounds -04:14 +450 -1: test/integer_test.dart: integer widths postgres Dart snapshots and imports retain widths without confusing value codecs -04:15 +451 -1: test/integer_test.dart: integer widths postgres widening and narrowing use reviewed conversions with atomic failure recovery -04:15 +452 -1: test/integer_test.dart: integer widths postgres column renames preserve width metadata and range constraints -04:15 +453 -1: test/integer_test.dart: integer widths postgres historical bounded backfills verify widths and resume normally -04:15 +454 -1: test/integer_test.dart: integer widths postgres changing an identity width preserves generation and rejects out-of-range explicit IDs -04:15 +455 -1: test/integer_test.dart: analyzer validates width annotations and integer-backed domain codecs -04:17 +456 -1: test/integer_test.dart: width declarations reject wrong kinds and ranges without changing default snapshots -04:17 +457 -1: loading test/relation_test.dart -04:17 +457 -1: test/relation_test.dart: sqlite automatic unique joins preserve root pagination in one statement -04:17 +458 -1: test/relation_test.dart: sqlite all-null projections still distinguish a present row from absence -04:17 +459 -1: test/relation_test.dart: sqlite explicit batch deduplicates composite keys and skips partial nulls -04:17 +460 -1: test/relation_test.dart: sqlite composite chunks include filter and window parameters with nested joins -04:17 +461 -1: test/relation_test.dart: sqlite nested self joins and two edges to the same table use distinct aliases -04:17 +462 -1: test/relation_test.dart: sqlite batch collection below a joined optional parent loads on the same plan -04:17 +463 -1: test/relation_test.dart: sqlite join refuses unproven cardinality while automatic batch checks it -04:17 +464 -1: test/relation_test.dart: sqlite filters and to-one pagination do not drop root rows -04:17 +465 -1: test/relation_test.dart: sqlite joined records survive CTE projection and cursor decoding -04:17 +466 -1: test/relation_test.dart: sqlite required tests row presence independently of nullable selected values -04:17 +467 -1: test/relation_test.dart: sqlite relation aggregates restore the alias used by a joined selection -04:17 +468 -1: test/relation_test.dart: sqlite reusing an identical edge deduplicates its join -04:17 +469 -1: test/relation_test.dart: sqlite different filters need fresh relation occurrences -04:17 +470 -1: test/relation_test.dart: sqlite large composite batches avoid expression-depth failure -04:17 +471 -1: test/relation_test.dart: postgres automatic unique joins preserve root pagination in one statement -04:17 +472 -1: test/relation_test.dart: postgres all-null projections still distinguish a present row from absence -04:17 +473 -1: test/relation_test.dart: postgres explicit batch deduplicates composite keys and skips partial nulls -04:17 +474 -1: test/relation_test.dart: postgres composite chunks include filter and window parameters with nested joins -04:17 +475 -1: test/relation_test.dart: postgres nested self joins and two edges to the same table use distinct aliases -04:17 +476 -1: test/relation_test.dart: postgres batch collection below a joined optional parent loads on the same plan -04:17 +477 -1: test/relation_test.dart: postgres join refuses unproven cardinality while automatic batch checks it -04:17 +478 -1: test/relation_test.dart: postgres filters and to-one pagination do not drop root rows -04:17 +479 -1: test/relation_test.dart: postgres joined records survive CTE projection and cursor decoding -04:17 +480 -1: test/relation_test.dart: postgres required tests row presence independently of nullable selected values -04:17 +481 -1: test/relation_test.dart: postgres relation aggregates restore the alias used by a joined selection -04:17 +482 -1: test/relation_test.dart: postgres reusing an identical edge deduplicates its join -04:17 +483 -1: test/relation_test.dart: postgres different filters need fresh relation occurrences -04:17 +484 -1: test/relation_test.dart: postgres large composite batches avoid expression-depth failure -04:17 +485 -1: loading test/generated_database_test.dart -04:17 +485 -1: test/generated_database_test.dart: (setUpAll) -04:17 +485 -1: test/generated_database_test.dart: generated sqlite generated client, DDL, relations, dates and patches work together -04:17 +486 -1: test/generated_database_test.dart: generated sqlite planning is read-only and repeat migration is a no-op -04:17 +487 -1: test/generated_database_test.dart: generated sqlite compiled physical snapshot and full managed catalog comparison -04:17 +488 -1: test/generated_database_test.dart: generated sqlite baseline preserves existing rows and verifies keys before recording history -04:17 +489 -1: test/generated_database_test.dart: generated sqlite full verification sees defaults and additional unique constraints -04:17 +490 -1: test/generated_database_test.dart: generated sqlite history tampering and missing applied migrations fail -04:17 +491 -1: test/generated_database_test.dart: generated sqlite failed DDL rolls back schema and history atomically -04:17 +492 -1: test/generated_database_test.dart: generated sqlite actual column drift is detected independently of history -04:17 +493 -1: test/generated_database_test.dart: generated sqlite transaction control cannot be hidden behind SQL comments -04:18 +494 -1: test/generated_database_test.dart: generated sqlite concurrent migration callers serialize against shared history -04:18 +495 -1: test/generated_database_test.dart: generated postgres generated client, DDL, relations, dates and patches work together -04:18 +496 -1: test/generated_database_test.dart: generated postgres planning is read-only and repeat migration is a no-op -04:18 +497 -1: test/generated_database_test.dart: generated postgres compiled physical snapshot and full managed catalog comparison -04:18 +498 -1: test/generated_database_test.dart: generated postgres baseline preserves existing rows and verifies keys before recording history -04:18 +499 -1: test/generated_database_test.dart: generated postgres full verification sees defaults and additional unique constraints -04:18 +500 -1: test/generated_database_test.dart: generated postgres history tampering and missing applied migrations fail -04:18 +501 -1: test/generated_database_test.dart: generated postgres failed DDL rolls back schema and history atomically -04:18 +502 -1: test/generated_database_test.dart: generated postgres actual column drift is detected independently of history -04:18 +503 -1: test/generated_database_test.dart: generated postgres transaction control cannot be hidden behind SQL comments -04:18 +504 -1: test/generated_database_test.dart: generated postgres concurrent migration callers serialize against shared history -04:18 +505 -1: test/generated_database_test.dart: (tearDownAll) -04:18 +505 -1: loading test/import_test.dart -04:18 +505 -1: test/import_test.dart: catalog import sqlite imports records, identities, defaults and constraints then baselines unchanged rows -04:22 +506 -1: test/import_test.dart: catalog import sqlite normalizes reserved, colliding and hostile identifiers without changing physical names -04:23 +507 -1: test/import_test.dart: catalog import sqlite composite and self relations preserve key order and unique indexes -04:24 +508 -1: test/import_test.dart: catalog import sqlite unsupported storage excludes its table while computed SQL imports with a review note -04:24 +509 -1: test/import_test.dart: catalog import sqlite read-only snapshot and selected scope reject temporary shadowing and active transactions -04:24 +510 -1: test/import_test.dart: catalog import sqlite physical storage mapping does not infer domain types from data -04:25 +511 -1: test/import_test.dart: catalog import sqlite views triggers and specialized indexes remain in the review report -04:25 +512 -1: test/import_test.dart: catalog import sqlite nullable primary keys, virtual tables and their storage tables need explicit handling -04:25 +513 -1: test/import_test.dart: catalog import sqlite foreign keys with coercible but different Dart types are reported -04:26 +514 -1: test/import_test.dart: catalog import postgres imports records, identities, defaults and constraints then baselines unchanged rows -04:27 +515 -1: test/import_test.dart: catalog import postgres normalizes reserved, colliding and hostile identifiers without changing physical names -04:28 +516 -1: test/import_test.dart: catalog import postgres composite and self relations preserve key order and unique indexes -04:29 +517 -1: test/import_test.dart: catalog import postgres unsupported storage excludes its table while computed SQL imports with a review note -04:29 +518 -1: test/import_test.dart: catalog import postgres read-only snapshot and selected scope reject temporary shadowing and active transactions -04:29 +519 -1: test/import_test.dart: catalog import postgres physical storage mapping does not infer domain types from data -04:29 +520 -1: test/import_test.dart: catalog import postgres views triggers and specialized indexes remain in the review report -04:30 +521 -1: test/import_test.dart: catalog import postgres policy reports retain roles, commands, mode and row-security state -04:30 +522 -1: test/import_test.dart: catalog import postgres always identity and inherited tables are not flattened into ordinary writable models -04:30 +523 -1: loading test/database_test.dart -04:30 +523 -1: test/database_test.dart: sqlite typed CRUD, exact projection and parameter binding -04:30 +524 -1: test/database_test.dart: sqlite record and DTO mapping run only when decoding -04:30 +525 -1: test/database_test.dart: sqlite immutable filters, empty IN, null and pagination -04:30 +526 -1: test/database_test.dart: sqlite cross-table identity and duplicate assignments rejected before IO -04:30 +527 -1: test/database_test.dart: sqlite commit, rollback, savepoint recovery and escaped session -04:30 +528 -1: test/database_test.dart: sqlite caught statement failures cannot masquerade as a commit -04:30 +529 -1: test/database_test.dart: sqlite recoverable constraint errors stay within their savepoint -04:30 +530 -1: test/database_test.dart: sqlite unawaited work is drained and rolled back -04:30 +531 -1: test/database_test.dart: sqlite close waits for active operations and rejects further use -04:30 +532 -1: test/database_test.dart: sqlite leased sessions retain one connection across transactions and expire -04:30 +533 -1: test/database_test.dart: sqlite native upsert updates atomically using existing and incoming values -04:30 +534 -1: test/database_test.dart: sqlite bulk inserts split at parameter limits and opt into returning -04:30 +535 -1: test/database_test.dart: sqlite a failed later bulk chunk rolls back all previous chunks -04:30 +536 -1: test/database_test.dart: sqlite bulk optional field shapes preserve defaults and explicit null -04:30 +537 -1: test/database_test.dart: sqlite relations CTEs preserve SQL column identity through mapped projections -04:30 +538 -1: test/database_test.dart: sqlite relations explicit aliases support multiple joins and self joins -04:30 +539 -1: test/database_test.dart: sqlite relations left join distinguishes a missing row from an all-null selection -04:30 +540 -1: test/database_test.dart: sqlite relations correlated scalar, IN and EXISTS use nested SQL scopes -04:30 +541 -1: test/database_test.dart: sqlite relations grouping is checked and window functions preserve row cardinality -04:30 +542 -1: test/database_test.dart: sqlite relations root pagination and each parent limit use two queries -04:30 +543 -1: test/database_test.dart: sqlite relations empty roots skip child queries; empty lists remain typed lists -04:30 +544 -1: test/database_test.dart: sqlite relations nested relation projections and optional all-null projection -04:30 +545 -1: test/database_test.dart: sqlite relations correlated any, none, count and every with SQL nulls -04:30 +546 -1: test/database_test.dart: sqlite relations relation parameters bind correctly in projection and filter -04:30 +547 -1: test/database_test.dart: sqlite keyset pagination uses a unique tie breaker in mixed directions -04:30 +548 -1: test/database_test.dart: sqlite keyset pagination handles null order and validates versioned tokens -04:30 +549 -1: test/database_test.dart: postgres typed CRUD, exact projection and parameter binding -04:30 +550 -1: test/database_test.dart: postgres record and DTO mapping run only when decoding -04:30 +551 -1: test/database_test.dart: postgres immutable filters, empty IN, null and pagination -04:30 +552 -1: test/database_test.dart: postgres cross-table identity and duplicate assignments rejected before IO -04:30 +553 -1: test/database_test.dart: postgres commit, rollback, savepoint recovery and escaped session -04:30 +554 -1: test/database_test.dart: postgres caught statement failures cannot masquerade as a commit -04:31 +555 -1: test/database_test.dart: postgres recoverable constraint errors stay within their savepoint -04:31 +556 -1: test/database_test.dart: postgres unawaited work is drained and rolled back -04:31 +557 -1: test/database_test.dart: postgres close waits for active operations and rejects further use -04:31 +558 -1: test/database_test.dart: postgres leased sessions retain one connection across transactions and expire -04:31 +559 -1: test/database_test.dart: postgres native upsert updates atomically using existing and incoming values -04:31 +560 -1: test/database_test.dart: postgres bulk inserts split at parameter limits and opt into returning -04:31 +561 -1: test/database_test.dart: postgres a failed later bulk chunk rolls back all previous chunks -04:31 +562 -1: test/database_test.dart: postgres bulk optional field shapes preserve defaults and explicit null -04:31 +563 -1: test/database_test.dart: postgres relations CTEs preserve SQL column identity through mapped projections -04:31 +564 -1: test/database_test.dart: postgres relations explicit aliases support multiple joins and self joins -04:31 +565 -1: test/database_test.dart: postgres relations left join distinguishes a missing row from an all-null selection -04:31 +566 -1: test/database_test.dart: postgres relations correlated scalar, IN and EXISTS use nested SQL scopes -04:31 +567 -1: test/database_test.dart: postgres relations grouping is checked and window functions preserve row cardinality -04:31 +568 -1: test/database_test.dart: postgres relations root pagination and each parent limit use two queries -04:31 +569 -1: test/database_test.dart: postgres relations empty roots skip child queries; empty lists remain typed lists -04:31 +570 -1: test/database_test.dart: postgres relations nested relation projections and optional all-null projection -04:31 +571 -1: test/database_test.dart: postgres relations correlated any, none, count and every with SQL nulls -04:31 +572 -1: test/database_test.dart: postgres relations relation parameters bind correctly in projection and filter -04:31 +573 -1: test/database_test.dart: postgres keyset pagination uses a unique tie breaker in mixed directions -04:31 +574 -1: test/database_test.dart: postgres keyset pagination handles null order and validates versioned tokens -04:31 +575 -1: loading test/union_test.dart -04:31 +575 -1: test/union_test.dart: union sqlite SQL duplicate and NULL semantics use one statement -04:31 +576 -1: test/union_test.dart: union sqlite heterogeneous records map only after SQL union and final pagination -04:31 +577 -1: test/union_test.dart: union sqlite operand limits, order, parameters and final filter stay scoped -04:31 +578 -1: test/union_test.dart: union sqlite repeated SQL expressions preserve positional columns -04:31 +579 -1: test/union_test.dart: union sqlite nested sets preserve association, CTE exports and streaming -04:31 +580 -1: test/union_test.dart: union sqlite same CTE names in distinct operands stay in separate scopes -04:31 +581 -1: test/union_test.dart: union sqlite SQL scalar, IN subqueries, aggregate and zero-row results compose -04:31 +582 -1: test/union_test.dart: union sqlite grouped branches and window branches remain SQL queries -04:31 +583 -1: test/union_test.dart: union sqlite bound constant projections retain Dart types -04:31 +584 -1: test/union_test.dart: union sqlite Dart maps and relation selections are rejected before execution -04:31 +585 -1: test/union_test.dart: union sqlite codecs and nullability cannot silently change across operands -04:31 +586 -1: test/union_test.dart: union sqlite shared domain codecs work, and nullable wrappers retain decoding semantics -04:31 +587 -1: test/union_test.dart: union sqlite export references and outer join nullability are guarded -04:31 +588 -1: test/union_test.dart: union sqlite compound mutations and mixed sessions fail before SQL -04:31 +589 -1: test/union_test.dart: union sqlite all record arities use typed SQL decoding -04:31 +590 -1: test/union_test.dart: union sqlite subscription observes committed changes in both operands -04:31 +591 -1: test/union_test.dart: union postgres SQL duplicate and NULL semantics use one statement -04:31 +592 -1: test/union_test.dart: union postgres heterogeneous records map only after SQL union and final pagination -04:31 +593 -1: test/union_test.dart: union postgres operand limits, order, parameters and final filter stay scoped -04:31 +594 -1: test/union_test.dart: union postgres repeated SQL expressions preserve positional columns -04:31 +595 -1: test/union_test.dart: union postgres nested sets preserve association, CTE exports and streaming -04:31 +596 -1: test/union_test.dart: union postgres same CTE names in distinct operands stay in separate scopes -04:31 +597 -1: test/union_test.dart: union postgres SQL scalar, IN subqueries, aggregate and zero-row results compose -04:31 +598 -1: test/union_test.dart: union postgres grouped branches and window branches remain SQL queries -04:31 +599 -1: test/union_test.dart: union postgres bound constant projections retain Dart types -04:31 +600 -1: test/union_test.dart: union postgres Dart maps and relation selections are rejected before execution -04:31 +601 -1: test/union_test.dart: union postgres codecs and nullability cannot silently change across operands -04:31 +602 -1: test/union_test.dart: union postgres shared domain codecs work, and nullable wrappers retain decoding semantics -04:31 +603 -1: test/union_test.dart: union postgres export references and outer join nullability are guarded -04:31 +604 -1: test/union_test.dart: union postgres compound mutations and mixed sessions fail before SQL -04:31 +605 -1: test/union_test.dart: union postgres all record arities use typed SQL decoding -04:31 +606 -1: test/union_test.dart: union postgres subscription observes committed changes in both operands -04:31 +607 -1: loading test/import_cli_test.dart -04:32 +607 -1: test/import_cli_test.dart: CLI sqlite imports reviews generates and baselines existing data without replacing files -05:26 +608 -1: test/import_cli_test.dart: CLI postgres imports reviews generates and baselines existing data without replacing files -06:16 +609 -1: loading test/decimal_division_test.dart -06:16 +609 -1: test/decimal_division_test.dart: decimal division sqlite signed modes and scales match exact integer arithmetic -06:17 +610 -1: test/decimal_division_test.dart: decimal division sqlite exact defaults reject lost digits and zero divisors, NULL propagates -06:17 +611 -1: test/decimal_division_test.dart: decimal division sqlite precision exceeds native division scale without double rounding -06:17 +612 -1: test/decimal_division_test.dart: decimal division sqlite huge remainders avoid overflowing intermediate scaled products -06:40 +613 -1: test/decimal_division_test.dart: decimal division sqlite outermost integer scale and result overflow are explicit -06:41 +614 -1: test/decimal_division_test.dart: decimal division sqlite aggregates windows CTEs predicates and unions compose -06:41 +615 -1: test/decimal_division_test.dart: decimal division sqlite expression assignments and transaction rollback preserve values -06:41 +616 -1: test/decimal_division_test.dart: decimal division sqlite window filtering ordering DISTINCT and pagination keep their SQL order -06:41 +617 -1: test/decimal_division_test.dart: decimal division sqlite windows over grouped aggregates run after HAVING -06:41 +618 -1: test/decimal_division_test.dart: decimal division sqlite window results compose through CTEs sets and correlated subqueries -06:41 +619 -1: test/decimal_division_test.dart: decimal division sqlite batched relations retain per-parent window pagination -06:41 +620 -1: test/decimal_division_test.dart: decimal division postgres signed modes and scales match exact integer arithmetic -06:41 +621 -1: test/decimal_division_test.dart: decimal division postgres exact defaults reject lost digits and zero divisors, NULL propagates -06:41 +622 -1: test/decimal_division_test.dart: decimal division postgres precision exceeds native division scale without double rounding -06:41 +623 -1: test/decimal_division_test.dart: decimal division postgres huge remainders avoid overflowing intermediate scaled products -07:03 +624 -1: test/decimal_division_test.dart: decimal division postgres outermost integer scale and result overflow are explicit -07:03 +625 -1: test/decimal_division_test.dart: decimal division postgres aggregates windows CTEs predicates and unions compose -07:03 +626 -1: test/decimal_division_test.dart: decimal division postgres expression assignments and transaction rollback preserve values -07:03 +627 -1: test/decimal_division_test.dart: decimal division postgres window filtering ordering DISTINCT and pagination keep their SQL order -07:03 +628 -1: test/decimal_division_test.dart: decimal division postgres windows over grouped aggregates run after HAVING -07:03 +629 -1: test/decimal_division_test.dart: decimal division postgres window results compose through CTEs sets and correlated subqueries -07:03 +630 -1: test/decimal_division_test.dart: decimal division postgres batched relations retain per-parent window pagination -07:03 +631 -1: test/decimal_division_test.dart: decimal division postgres volatile operands are evaluated exactly once each -07:03 +632 -1: loading test/cli_test.dart -07:03 +632 -1: test/cli_test.dart: Dart CLI generates, reviews, upgrades and verifies constraints without overwriting history -08:16 +633 -1: test/cli_test.dart: Dart CLI baselines existing rows; read-only connections and invalid options reject writes -08:45 +634 -1: test/cli_test.dart: Dart PostgreSQL configuration supports checked nontransactional recovery operations -09:30 +635 -1: loading test/decimal_test.dart -09:30 +635 -1: test/decimal_test.dart: exact decimal sqlite generated writes, defaults, patches and projections preserve all digits -09:30 +636 -1: test/decimal_test.dart: exact decimal sqlite numeric ordering, predicates, IN, distinct and stable cursors -09:30 +637 -1: test/decimal_test.dart: exact decimal sqlite sums, min/max, grouped aggregates and sliding windows stay exact -09:30 +638 -1: test/decimal_test.dart: exact decimal sqlite computed CTEs, union results, subqueries and streaming keep decimal semantics -09:30 +639 -1: test/decimal_test.dart: exact decimal sqlite numeric primary keys, foreign keys, joined and batched relations -09:30 +640 -1: test/decimal_test.dart: exact decimal sqlite snapshots, catalog verification and imported declarations retain decimal storage -09:32 +641 -1: test/decimal_test.dart: exact decimal sqlite aggregate intermediate overflow may cancel before a valid final result -09:32 +642 -1: test/decimal_test.dart: exact decimal sqlite historical decimal keys resume backfills without lexical ordering or precision loss -09:32 +643 -1: test/decimal_test.dart: exact decimal sqlite reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:32 +644 -1: test/decimal_test.dart: exact decimal sqlite invalid external values fail decoding; later valid queries still work -09:32 +645 -1: test/decimal_test.dart: exact decimal postgres generated writes, defaults, patches and projections preserve all digits -09:32 +646 -1: test/decimal_test.dart: exact decimal postgres numeric ordering, predicates, IN, distinct and stable cursors -09:32 +647 -1: test/decimal_test.dart: exact decimal postgres sums, min/max, grouped aggregates and sliding windows stay exact -09:32 +648 -1: test/decimal_test.dart: exact decimal postgres computed CTEs, union results, subqueries and streaming keep decimal semantics -09:32 +649 -1: test/decimal_test.dart: exact decimal postgres numeric primary keys, foreign keys, joined and batched relations -09:32 +650 -1: test/decimal_test.dart: exact decimal postgres snapshots, catalog verification and imported declarations retain decimal storage -09:33 +651 -1: test/decimal_test.dart: exact decimal postgres aggregate intermediate overflow may cancel before a valid final result -09:33 +652 -1: test/decimal_test.dart: exact decimal postgres historical decimal keys resume backfills without lexical ordering or precision loss -09:33 +653 -1: test/decimal_test.dart: exact decimal postgres reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:33 +654 -1: test/decimal_test.dart: exact decimal postgres invalid external values fail decoding; later valid queries still work -09:33 +655 -1: test/decimal_test.dart: decimal SQL rejects drivers without exact operations before execution -09:33 +656 -1: test/decimal_test.dart: SQLite recognizes only column decimal collations, and detects drift -09:33 +657 -1: loading test/watch_test.dart -09:33 +657 -1: test/watch_test.dart: independent native SQLite workers require explicit notification -09:33 +658 -1: test/watch_test.dart: watch sqlite lazy initial snapshot and relevant table invalidation -09:33 +659 -1: test/watch_test.dart: watch sqlite transactions publish one snapshot after commit and none on rollback -09:33 +660 -1: test/watch_test.dart: watch sqlite savepoint changes merge on release and disappear on rollback -09:33 +661 -1: test/watch_test.dart: watch sqlite failed statements, zero-row writes and conflict no-ops do not refresh -09:33 +662 -1: test/watch_test.dart: watch sqlite batch writes and upsert returning publish committed changes -09:33 +663 -1: test/watch_test.dart: watch sqlite joined and nested batch relations track child writes -09:33 +664 -1: test/watch_test.dart: watch sqlite relation predicates, SQL subqueries and CTE sources are dependencies -09:33 +665 -1: test/watch_test.dart: watch sqlite declared cascade deletion invalidates a child-only query -09:33 +666 -1: test/watch_test.dart: watch sqlite CASCADE tracks declared transitive deletion effects -09:33 +667 -1: test/watch_test.dart: watch sqlite SET NULL tracks declared transitive deletion effects -09:33 +668 -1: test/watch_test.dart: watch sqlite explicit JOINs and joined CTE definitions track both physical sources -09:33 +669 -1: test/watch_test.dart: watch sqlite raw SQL declares changed and read tables; explicit invalidation follows transactions -09:33 +670 -1: test/watch_test.dart: watch sqlite paused listeners coalesce writes and stop fetching until resumed -09:33 +671 -1: test/watch_test.dart: watch sqlite invalidation during a read suppresses the stale snapshot and refetches -09:33 +672 -1: test/watch_test.dart: watch sqlite decoding errors are reported and a later write can recover -09:33 +673 -1: test/watch_test.dart: watch sqlite separate subscriptions and views of one driver receive changes -09:33 +674 -1: test/watch_test.dart: watch sqlite watching leased sessions is rejected and cancelled tokens close the stream -09:33 +675 -1: test/watch_test.dart: watch sqlite database close finishes watchers even when a listener is paused -09:33 +676 -1: test/watch_test.dart: watch sqlite cancelling an active watched query interrupts SQL and releases its lease -09:33 +677 -1: test/watch_test.dart: watch postgres lazy initial snapshot and relevant table invalidation -09:34 +678 -1: test/watch_test.dart: watch postgres transactions publish one snapshot after commit and none on rollback -09:34 +679 -1: test/watch_test.dart: watch postgres savepoint changes merge on release and disappear on rollback -09:34 +680 -1: test/watch_test.dart: watch postgres failed statements, zero-row writes and conflict no-ops do not refresh -09:34 +681 -1: test/watch_test.dart: watch postgres batch writes and upsert returning publish committed changes -09:34 +682 -1: test/watch_test.dart: watch postgres joined and nested batch relations track child writes -09:34 +683 -1: test/watch_test.dart: watch postgres relation predicates, SQL subqueries and CTE sources are dependencies -09:34 +684 -1: test/watch_test.dart: watch postgres declared cascade deletion invalidates a child-only query -09:34 +685 -1: test/watch_test.dart: watch postgres CASCADE tracks declared transitive deletion effects -09:34 +686 -1: test/watch_test.dart: watch postgres SET NULL tracks declared transitive deletion effects -09:34 +687 -1: test/watch_test.dart: watch postgres explicit JOINs and joined CTE definitions track both physical sources -09:34 +688 -1: test/watch_test.dart: watch postgres raw SQL declares changed and read tables; explicit invalidation follows transactions -09:34 +689 -1: test/watch_test.dart: watch postgres paused listeners coalesce writes and stop fetching until resumed -09:34 +690 -1: test/watch_test.dart: watch postgres invalidation during a read suppresses the stale snapshot and refetches -09:34 +691 -1: test/watch_test.dart: watch postgres decoding errors are reported and a later write can recover -09:34 +692 -1: test/watch_test.dart: watch postgres separate subscriptions and views of one driver receive changes -09:34 +693 -1: test/watch_test.dart: watch postgres watching leased sessions is rejected and cancelled tokens close the stream -09:34 +694 -1: test/watch_test.dart: watch postgres database close finishes watchers even when a listener is paused -09:34 +695 -1: test/watch_test.dart: watch postgres cancelling an active watched query interrupts SQL and releases its lease -09:34 +696 -1: test/watch_test.dart: watch postgres lost commit acknowledgement triggers a conservative re-read -09:34 +697 -1: test/watch_test.dart: watch postgres independent connections require an explicit external-write notification -09:34 +698 -1: loading test/backfill_test.dart -09:34 +698 -1: test/backfill_test.dart: backfill sqlite chunks pause and resume from a checksummed historical declaration -09:34 +699 -1: test/backfill_test.dart: backfill sqlite failed chunk rolls back both writes and cursor, then resumes after repair -09:34 +700 -1: test/backfill_test.dart: backfill sqlite failed completion proof preserves finished chunks without replay -09:34 +701 -1: test/backfill_test.dart: backfill sqlite frozen upper bound and completion proof detect incompatible new writes -09:34 +702 -1: test/backfill_test.dart: backfill sqlite competing runners share committed chunks without duplicate updates -09:34 +703 -1: test/backfill_test.dart: backfill sqlite lost chunk commit acknowledgement preserves the committed frontier -09:34 +704 -1: test/backfill_test.dart: backfill sqlite old checkpoint tables upgrade on apply; completed reads remain nonmutating -09:34 +705 -1: test/backfill_test.dart: backfill sqlite completion conditions reject numeric results instead of truthiness -09:34 +706 -1: test/backfill_test.dart: backfill sqlite empty input still requires the completion condition -09:34 +707 -1: test/backfill_test.dart: backfill sqlite later ordinary migrations retain group atomicity after a completed backfill -09:34 +708 -1: test/backfill_test.dart: backfill sqlite AFTER triggers cannot silently move keys past a committed cursor -09:34 +709 -1: test/backfill_test.dart: backfill sqlite corrupt checkpoint dimensions and invalid run limits are rejected -09:34 +710 -1: test/backfill_test.dart: backfill sqlite rebuild, chunked backfill and required-column rebuild preserve FK enforcement -09:34 +711 -1: test/backfill_test.dart: backfill sqlite CLI previews non-atomic work and reports a bounded run as incomplete -09:59 +712 -1: test/backfill_test.dart: backfill sqlite temporary tables cannot redirect the historical backfill target -09:59 +713 -1: test/backfill_test.dart: backfill sqlite temporary objects cannot impersonate durable migration metadata -09:59 +714 -1: test/backfill_test.dart: backfill sqlite committed chunks invalidate relevant typed query subscriptions -09:59 +715 -1: test/backfill_test.dart: backfill sqlite real process exit after second chunk write resumes correctly -10:06 +716 -1: test/backfill_test.dart: backfill sqlite real process exit after second chunk commit resumes correctly -10:13 +717 -1: test/backfill_test.dart: backfill postgres chunks pause and resume from a checksummed historical declaration -10:13 +718 -1: test/backfill_test.dart: backfill postgres failed chunk rolls back both writes and cursor, then resumes after repair -10:13 +719 -1: test/backfill_test.dart: backfill postgres failed completion proof preserves finished chunks without replay -10:13 +720 -1: test/backfill_test.dart: backfill postgres frozen upper bound and completion proof detect incompatible new writes -10:13 +721 -1: test/backfill_test.dart: backfill postgres competing runners share committed chunks without duplicate updates -10:13 +722 -1: test/backfill_test.dart: backfill postgres lost chunk commit acknowledgement preserves the committed frontier -10:13 +723 -1: test/backfill_test.dart: backfill postgres old checkpoint tables upgrade on apply; completed reads remain nonmutating -10:13 +724 -1: test/backfill_test.dart: backfill postgres completion conditions reject numeric results instead of truthiness -10:13 +725 -1: test/backfill_test.dart: backfill postgres empty input still requires the completion condition -10:13 +726 -1: test/backfill_test.dart: backfill postgres later ordinary migrations retain group atomicity after a completed backfill -10:13 +727 -1: test/backfill_test.dart: backfill postgres AFTER triggers cannot silently move keys past a committed cursor -10:13 +728 -1: test/backfill_test.dart: backfill postgres corrupt checkpoint dimensions and invalid run limits are rejected -10:14 +729 -1: test/backfill_test.dart: backfill postgres CLI previews non-atomic work and reports a bounded run as incomplete -10:36 +730 -1: test/backfill_test.dart: backfill postgres temporary tables cannot redirect the historical backfill target -10:37 +731 -1: test/backfill_test.dart: backfill postgres temporary objects cannot impersonate durable migration metadata -10:37 +732 -1: test/backfill_test.dart: backfill postgres row security cannot hide unfinished rows from the completion proof -10:37 +733 -1: test/backfill_test.dart: backfill postgres inherited duplicate primary keys cannot advance a cursor -10:37 +734 -1: test/backfill_test.dart: backfill postgres committed chunks invalidate relevant typed query subscriptions -10:37 +735 -1: test/backfill_test.dart: backfill postgres real process exit after second chunk write resumes correctly -10:44 +736 -1: test/backfill_test.dart: backfill postgres real process exit after second chunk commit resumes correctly -10:50 +737 -1: loading test/custom_codec_test.dart -10:50 +737 -1: test/custom_codec_test.dart: (setUpAll) -10:50 +737 -1: test/custom_codec_test.dart: custom codecs sqlite generated custom values round trip through create, select and patch -10:50 +738 -1: test/custom_codec_test.dart: custom codecs sqlite enum labels are stable text and unknown labels fail decoding -10:50 +739 -1: test/custom_codec_test.dart: custom codecs sqlite domain validation and bound parameters apply to writes and predicates -10:50 +740 -1: test/custom_codec_test.dart: custom codecs sqlite JSON documents distinguish SQL NULL, JSON null and string scalars -10:50 +741 -1: test/custom_codec_test.dart: custom codecs sqlite nullable structured codec rejects JSON null instead of masking invalid data -10:50 +742 -1: test/custom_codec_test.dart: custom codecs sqlite custom IDs and domain projections work across joined and batched relations -10:50 +743 -1: test/custom_codec_test.dart: custom codecs sqlite custom values participate in atomic batch writes and native upsert -10:50 +744 -1: test/custom_codec_test.dart: custom codecs sqlite custom IDs and enums survive cursor transport and streaming decoders -10:50 +745 -1: test/custom_codec_test.dart: custom codecs sqlite migration snapshots retain physical storage without executing domain codecs -10:50 +746 -1: test/custom_codec_test.dart: custom codecs postgres generated custom values round trip through create, select and patch -10:50 +747 -1: test/custom_codec_test.dart: custom codecs postgres enum labels are stable text and unknown labels fail decoding -10:50 +748 -1: test/custom_codec_test.dart: custom codecs postgres domain validation and bound parameters apply to writes and predicates -10:50 +749 -1: test/custom_codec_test.dart: custom codecs postgres JSON documents distinguish SQL NULL, JSON null and string scalars -10:50 +750 -1: test/custom_codec_test.dart: custom codecs postgres nullable structured codec rejects JSON null instead of masking invalid data -10:50 +751 -1: test/custom_codec_test.dart: custom codecs postgres custom IDs and domain projections work across joined and batched relations -10:50 +752 -1: test/custom_codec_test.dart: custom codecs postgres custom values participate in atomic batch writes and native upsert -10:50 +753 -1: test/custom_codec_test.dart: custom codecs postgres custom IDs and enums survive cursor transport and streaming decoders -10:50 +754 -1: test/custom_codec_test.dart: custom codecs postgres migration snapshots retain physical storage without executing domain codecs -10:50 +755 -1: test/custom_codec_test.dart: (tearDownAll) -10:50 +755 -1: loading test/plan_test.dart -10:50 +755 -1: test/plan_test.dart: inspect sqlite reports composite key width and validates each potential batch -10:50 +756 -1: test/plan_test.dart: inspect sqlite compiles nested plans without acquiring or decoding -10:50 +757 -1: test/plan_test.dart: inspect sqlite distinguishes same-statement joins and skipped batches -10:50 +758 -1: test/plan_test.dart: inspect sqlite retains CTE, UNION and raw SQL boundaries -10:50 +759 -1: test/plan_test.dart: plan execution sqlite one-key SQL templates match executed SQL and internal slots -10:50 +760 -1: test/plan_test.dart: inspect postgres reports composite key width and validates each potential batch -10:50 +761 -1: test/plan_test.dart: inspect postgres compiles nested plans without acquiring or decoding -10:50 +762 -1: test/plan_test.dart: inspect postgres distinguishes same-statement joins and skipped batches -10:50 +763 -1: test/plan_test.dart: inspect postgres retains CTE, UNION and raw SQL boundaries -10:50 +764 -1: test/plan_test.dart: plan execution postgres one-key SQL templates match executed SQL and internal slots -10:51 +765 -1: loading test/check_test.dart -10:51 +765 -1: test/check_test.dart: CHECK sqlite generated named and unnamed constraints enforce values and permit NULL -10:51 +766 -1: test/check_test.dart: CHECK sqlite import preserves names and SQL then generates a matching snapshot -10:53 +767 -1: test/check_test.dart: CHECK sqlite add, change and remove constraints validate old rows atomically -10:53 +768 -1: test/check_test.dart: CHECK sqlite duplicate unnamed checks consume and drop exactly one catalog entry -10:53 +769 -1: test/check_test.dart: CHECK sqlite explicit checked renames preserve rows, incoming references, indexes and views -10:53 +770 -1: test/check_test.dart: CHECK sqlite verification preserves casts, precedence, literals and trailing comments -10:53 +771 -1: test/check_test.dart: CHECK sqlite quoted Unicode identifiers retain SQLite case distinctions -10:53 +772 -1: test/check_test.dart: CHECK sqlite undeclared CHECK is refused before a rebuild and remains enforced -10:53 +773 -1: test/check_test.dart: CHECK postgres generated named and unnamed constraints enforce values and permit NULL -10:53 +774 -1: test/check_test.dart: CHECK postgres import preserves names and SQL then generates a matching snapshot -10:54 +775 -1: test/check_test.dart: CHECK postgres add, change and remove constraints validate old rows atomically -10:54 +776 -1: test/check_test.dart: CHECK postgres duplicate unnamed checks consume and drop exactly one catalog entry -10:54 +777 -1: test/check_test.dart: CHECK postgres explicit checked renames preserve rows, incoming references, indexes and views -10:54 +778 -1: test/check_test.dart: CHECK postgres verification preserves casts, precedence, literals and trailing comments -10:54 +779 -1: test/check_test.dart: CHECK postgres catalog excludes unvalidated and NO INHERIT constraints -10:54 +780 -1: test/check_test.dart: CHECK postgres verification plans but does not execute volatile row expressions -10:54 +781 -1: test/check_test.dart: dialect overrides only alter the affected backend and preserve old snapshot JSON -10:54 +782 -1: loading test/temporal_test.dart -10:54 +782 -1: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: false -10:54 +783 -1: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: true -10:54 +784 -1: test/temporal_test.dart: Gregorian cycles, era boundaries and full finite date range -10:54 +785 -1: test/temporal_test.dart: local time precision, day endpoint and timestamp arithmetic -10:54 +786 -1: test/temporal_test.dart: PostgreSQL binary registry covers boundaries without DateTime -10:54 +787 -1: test/temporal_test.dart: local temporal sqlite generated writes, defaults, null patches and typed parameters -10:54 +788 -1: test/temporal_test.dart: local temporal sqlite finite extremes and microseconds survive native rows and streams -10:54 +789 -1: test/temporal_test.dart: local temporal sqlite calendar ordering, min/max, grouping and stable date cursors -10:54 +790 -1: test/temporal_test.dart: local temporal sqlite equivalent date keys drive batched relations and unique indexes -10:54 +791 -1: test/temporal_test.dart: local temporal sqlite time and local timestamp comparisons retain calendar semantics -10:54 +792 -1: test/temporal_test.dart: local temporal sqlite reviewed text-to-time migration rolls back duplicate clock values -10:54 +793 -1: test/temporal_test.dart: local temporal sqlite invalid external dates fail typed reads without damaging the connection -10:54 +794 -1: test/temporal_test.dart: local temporal sqlite snapshots, catalog import and regenerated defaults agree -10:57 +795 -1: test/temporal_test.dart: local temporal sqlite historical date keys resume a bounded backfill in calendar order -10:57 +796 -1: test/temporal_test.dart: local temporal postgres catalog retains time-truncating casts in expression defaults -10:57 +797 -1: test/temporal_test.dart: local temporal postgres generated writes, defaults, null patches and typed parameters -10:57 +798 -1: test/temporal_test.dart: local temporal postgres finite extremes and microseconds survive native rows and streams -10:57 +799 -1: test/temporal_test.dart: local temporal postgres calendar ordering, min/max, grouping and stable date cursors -10:57 +800 -1: test/temporal_test.dart: local temporal postgres equivalent date keys drive batched relations and unique indexes -10:57 +801 -1: test/temporal_test.dart: local temporal postgres time and local timestamp comparisons retain calendar semantics -10:57 +802 -1: test/temporal_test.dart: local temporal postgres reviewed text-to-time migration rolls back duplicate clock values -10:57 +803 -1: test/temporal_test.dart: local temporal postgres invalid external dates fail typed reads without damaging the connection -10:57 +804 -1: test/temporal_test.dart: local temporal postgres snapshots, catalog import and regenerated defaults agree -10:58 +805 -1: test/temporal_test.dart: local temporal postgres historical date keys resume a bounded backfill in calendar order -10:58 +806 -1: loading test/unconstrained_relation_test.dart -10:58 +806 -1: test/unconstrained_relation_test.dart: unconstrained sqlite catalogs contain no FK, implicit index or uniqueness promise -10:58 +807 -1: test/unconstrained_relation_test.dart: unconstrained sqlite adding and removing a real FK remains a reviewed physical migration -10:58 +808 -1: test/unconstrained_relation_test.dart: unconstrained sqlite unique joins preserve root rows, NULL components and missing targets -10:58 +809 -1: test/unconstrained_relation_test.dart: unconstrained sqlite required target and filter visibility remain explicit -10:58 +810 -1: test/unconstrained_relation_test.dart: unconstrained sqlite nonunique composite collections batch by tuple and page per parent -10:58 +811 -1: test/unconstrained_relation_test.dart: unconstrained sqlite to-one uses cardinality checks without assuming a unique lookup -10:58 +812 -1: test/unconstrained_relation_test.dart: unconstrained sqlite correlated filters and counts do not materialize related rows -10:58 +813 -1: test/unconstrained_relation_test.dart: unconstrained sqlite self relations and inverse collections share the existing planner -10:58 +814 -1: test/unconstrained_relation_test.dart: unconstrained sqlite delete has no cascade and watches track the joined target -10:58 +815 -1: test/unconstrained_relation_test.dart: unconstrained sqlite transaction reads observe writes and outer rollback restores targets -10:58 +816 -1: test/unconstrained_relation_test.dart: unconstrained sqlite streaming preserves optional and batched relation result shapes -10:58 +817 -1: test/unconstrained_relation_test.dart: unconstrained sqlite floating relation keys retain storage intent and key equality -10:58 +818 -1: test/unconstrained_relation_test.dart: unconstrained postgres catalogs contain no FK, implicit index or uniqueness promise -10:58 +819 -1: test/unconstrained_relation_test.dart: unconstrained postgres adding and removing a real FK remains a reviewed physical migration -10:58 +820 -1: test/unconstrained_relation_test.dart: unconstrained postgres unique joins preserve root rows, NULL components and missing targets -10:58 +821 -1: test/unconstrained_relation_test.dart: unconstrained postgres required target and filter visibility remain explicit -10:58 +822 -1: test/unconstrained_relation_test.dart: unconstrained postgres nonunique composite collections batch by tuple and page per parent -10:58 +823 -1: test/unconstrained_relation_test.dart: unconstrained postgres to-one uses cardinality checks without assuming a unique lookup -10:58 +824 -1: test/unconstrained_relation_test.dart: unconstrained postgres correlated filters and counts do not materialize related rows -10:58 +825 -1: test/unconstrained_relation_test.dart: unconstrained postgres self relations and inverse collections share the existing planner -10:58 +826 -1: test/unconstrained_relation_test.dart: unconstrained postgres delete has no cascade and watches track the joined target -10:58 +827 -1: test/unconstrained_relation_test.dart: unconstrained postgres transaction reads observe writes and outer rollback restores targets -10:58 +828 -1: test/unconstrained_relation_test.dart: unconstrained postgres streaming preserves optional and batched relation result shapes -10:58 +829 -1: test/unconstrained_relation_test.dart: unconstrained postgres floating relation keys retain storage intent and key equality -10:58 +830 -1: loading test/decimal_value_test.dart -10:58 +830 -1: test/decimal_value_test.dart: normalization, scientific notation, equality and hash are exact -10:58 +831 -1: test/decimal_value_test.dart: invalid and approximate inputs fail instead of silently rounding -10:58 +832 -1: test/decimal_value_test.dart: range boundaries are checked without binary numeric conversions -10:58 +833 -1: test/decimal_value_test.dart: addition, subtraction, multiplication and ordering keep every digit -10:58 +834 -1: test/decimal_value_test.dart: division requires an explicit scale and rejects implicit rounding -10:58 +835 -1: test/decimal_value_test.dart: rounding modes handle negative values, ties and negative scales -10:58 +836 -1: test/decimal_value_test.dart: small fixed-scale arithmetic agrees with independent integer arithmetic -10:58 +837 -1: Some tests failed. - -Failing tests: - test/instant_test.dart: historical migration checksum and timestamp tag remain immutable - -Consider enabling the flag chain-stack-traces to receive more detailed exceptions. -For example, 'dart test --chain-stack-traces'. diff --git a/research/validation/dart-migrations.json b/research/validation/dart-migrations.json deleted file mode 100644 index 03083675..00000000 --- a/research/validation/dart-migrations.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "capturedAt": "2026-09-15T11:25:08.799101+00:00", - "sourceCommit": "85c4e42bf82924f806d1ce79ba96f1f61af61b1f", - "dart": "3.13.3", - "postgres": "18.4", - "sqlite": "3.53.4", - "status": "passed", - "fullRun": { - "command": "ORM_TEST_POSTGRES= dart test -r expanded", - "exitCode": 1, - "passed": 837, - "failed": 1, - "duration": "10:58", - "failure": "test/instant_test.dart still read the removed 0001_legacy.json fixture. Corrected to use the static Dart migration fingerprint; the follow-up run passes.", - "sourceHashesBeforeFinalToolingChanges": { - "bin/orm.dart": "3b6930c60c81d3365b41078cd631418485f152ddac00e56276c8576d4346c836", - "lib/generate.dart": "e423c67f97cc089f8abb654f421e4f38c5e1a3a500316f7a8edfc7644a61952a", - "lib/src/generate/build.dart": "1e2d60e8c7bf96bdce213e447a03d609892e34536dd140fce0dfdc5e8f876d14", - "lib/src/generate/queries.dart": "00be85e6496c31f9ede18dd85dd8fc2556fa78b2dab28db3e668474153b76aef", - "test/build_runner_test.dart": "639b0d7fef472bda4f361b5de2dca45b97874915893302709b8bbc48f357e149", - "test/generator_test.dart": "01b0acea109071f4bbea2ac15631223e139dddb9fd754d3cbf0365567325cd32", - "test/named_sql_generation_test.dart": "2fb35d3b3e16a85c14530f35677ea0632f585476006c6e82182215b6831cc256", - "test/named_sql_test.dart": "a79ecdb1fb5b98ee79444c5055f1b6f88768727c919a7342709bae2a13fe78c0" - } - }, - "followup": { - "command": "ORM_TEST_POSTGRES= dart test test/instant_test.dart test/generator_test.dart test/named_sql_generation_test.dart test/named_sql_test.dart test/build_runner_test.dart -r expanded", - "exitCode": 0, - "passed": 76, - "duration": "01:33", - "scope": "Rechecks the corrected test, final source-output collision guard and removal of named-query JSON manifests, including live build/watch, freshness checks and native SQL structure validation." - }, - "aot": { - "command": "dart test test/migration_source_test.dart --name 'AOT migration bundle' -r expanded", - "exitCode": 0, - "passed": 1, - "scope": "Compiled CLI bundle after removing all consumer lib/bin Dart sources, pubspec and .dart_tool; offline validation, missing-file read-only rejection, apply/no-replay, schema verify and independent row/history readback." - }, - "backfillAot": { - "command": "dart build cli --target test/support/native_backfill.dart --output .dart_tool/backfill-aot && .dart_tool/backfill-aot/bundle/bin/native_backfill", - "exitCode": 0, - "scope": "Statically imported historical backfill, bounded pause, two concurrent runners, exact-once row update and startup compatibility." - }, - "browser": { - "report": "research/validation/browser.json", - "javascriptChecks": 19, - "wasmChecks": 19 - }, - "logs": { - "full": { - "path": "research/validation/dart-migrations-full.log", - "sha256": "5e50af0d80d77b3e6b4026a8183ab89f7b6a90229cebac226baebd5ba7defdb8" - }, - "followup": { - "path": "research/validation/dart-migrations-followup.log", - "sha256": "02a93a391310adfef2a6b6f0a8c0555c769721cbe3ba75b0dfa63c9e05e07623" - }, - "aot": { - "path": "research/validation/dart-migrations-aot.log", - "sha256": "b2abed7aff420857a9b576317fc0b0c0fd2d32de64a9dc41fd2e383d58344fae" - }, - "backfill-aot": { - "path": "research/validation/dart-migrations-backfill-aot.log", - "sha256": "b2ed0ba7416e8e9669b4dd5017a7d6bfeb54860a6204cd21e4ffb2f55ffdb5ae" - } - }, - "limits": [ - "The full run and focused follow-up are separate invocations; this is not a claim of a single all-green full run.", - "SQL strings and physical rename/conversion names still require migration review and database validation.", - "Internal checksum/checkpoint encoding and machine-readable reports still use JSON; no schema/migration/named-query JSON artifact reader remains." - ], - "example": [ - { - "command": "dart run example/migrate.dart check", - "exitCode": 0, - "report": { - "valid": true, - "migrations": [ - "0001_initial" - ] - } - }, - { - "command": "dart run example/migrate.dart apply", - "exitCode": 0, - "report": { - "applied": [ - "0001_initial" - ] - } - }, - { - "command": "dart run example/migrate.dart verify", - "exitCode": 0, - "report": { - "matches": true, - "differences": [], - "unmanaged": [] - } - }, - { - "command": "dart run example/migrate.dart apply", - "exitCode": 0, - "report": { - "applied": [] - } - } - ], - "android": { - "report": "research/validation/flutter.json", - "checks": [ - 4, - 17, - 11 - ], - "scope": "Real Android API 35 arm64 emulator. Debug version 1 install, release AOT version 2 upgrade retaining data, then distinct-process restart. Both APK ZIP inventories contain no migration JSON assets or Dart snapshot sources." - } -} diff --git a/research/validation/editor-recovery.json b/research/validation/editor-recovery.json deleted file mode 100644 index 71f4d70d..00000000 --- a/research/validation/editor-recovery.json +++ /dev/null @@ -1,891 +0,0 @@ -{ - "capturedAt": "2026-09-15T09:49:44.135320+00:00", - "runtimeSourceCommit": "20ffee7", - "sameSession": { - "command": "dart run tool/benchmark_editor.dart --same-session", - "exitCode": 255, - "attemptedModels": 10, - "completedScales": 0, - "status": "rename_timeout", - "timeoutSeconds": 60, - "failedMethod": "textDocument/rename", - "symbol": "primary_constructor_field", - "completedBeforeTimeout": [ - "five completion probes", - "four exact-code/range diagnostics", - "repair diagnostic", - "Record field rename unavailable" - ], - "log": "research/validation/editor-recovery.log", - "logSha256": "0bff8f1138af759bd52b8c46b9c312f9b6282f1202e20937319c3f47d0420690" - }, - "restartRecovery": { - "recordedAt": "2026-09-15T09:47:50.614525Z", - "runtimeSourceCommit": "20ffee74ea8d8bcc37de43448cf28837503ebfb9", - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "os": "Version 26.6.2 (Build 25G83)", - "cpu": "Apple M3 Max", - "smoke": true, - "sameSession": false, - "samplesPerWarmProbe": 2, - "harnessSha256": { - "tool/benchmark_editor.dart": "d38e9ad45f1b72937a8b054ab02ba7e481cfdbb47426c5b245d60b04706db69b", - "tool/src/editor_server.dart": "00e3cf5e7675166979cdb03a9d333754ee7aeb2874d6940d5d5ab6cff43a9ff8", - "tool/src/build_fixture.dart": "03a5d875d608fd25245b5da268d66ce4c0913ed288ac05270ae9522a68bb0ea9" - }, - "scope": "Direct stdio LSP client against the installed Dart Analysis Server, isolated consumer packages and real generated APIs; no database. Includes client transport/JSON costs, excludes GUI/plugin rendering and dependency download. Each scale uses one fresh server for completion/diagnostics and another for symbol renames. Startup and initial project analysis are measured separately from completion. First completion follows a fresh diagnostic for the incomplete source and is not a cold OS/cache measurement. Warm probes use unchanged documents after two unrecorded warmups. Diagnostic probes serialize changes and await a fresh notification; this SDK omits document versions. Final CLI analysis verifies the edited sources. Symbol-form probes compare language rename behavior only, not three ORM authoring frontends or identical generated APIs from each form.", - "limits": [ - "This capture restarts the server for renames and does not establish mixed-session rename after diagnostic recovery.", - "The named Record field remains unavailable for automatic rename in this SDK. A successful bounded sequence does not establish every IDE/plugin workflow." - ], - "results": [ - { - "models": 10, - "sessionPids": { - "diagnostics": 50960, - "renames": 50961 - }, - "sourceBytes": 1608, - "generatedBytes": 22953, - "initializeMicros": 33482, - "projectReadyMicros": 331734, - "completions": [ - { - "probe": "table_entries", - "analysisReadyMicros": 8780, - "firstRequestMicros": 2558, - "warm": { - "samplesMicros": [ - 1076, - 1142 - ], - "p50Micros": 1076, - "p95Micros": 1142 - }, - "response": { - "isIncomplete": false, - "itemCount": 31, - "items": [ - { - "label": "rows0", - "kind": 10, - "detail": "Rows0TableSet", - "labelDetails": { - "detail": " Rows0TableSet" - }, - "textEdit": { - "newText": "rows0", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - }, - { - "label": "rows9", - "kind": 10, - "detail": "Rows9TableSet", - "labelDetails": { - "detail": " Rows9TableSet" - }, - "textEdit": { - "newText": "rows9", - "range": { - "end": { - "character": 51, - "line": 3 - }, - "start": { - "character": 51, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "rows0", - "rows9" - ], - "forbiddenLabels": [] - }, - { - "probe": "schema_record_fields", - "analysisReadyMicros": 9476, - "firstRequestMicros": 790, - "warm": { - "samplesMicros": [ - 502, - 473 - ], - "p50Micros": 473, - "p95Micros": 502 - }, - "response": { - "isIncomplete": false, - "itemCount": 8, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "score", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "status", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 39, - "line": 25 - }, - "start": { - "character": 39, - "line": 25 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "query_fields", - "analysisReadyMicros": 8898, - "firstRequestMicros": 1580, - "warm": { - "samplesMicros": [ - 512, - 476 - ], - "p50Micros": 476, - "p95Micros": 512 - }, - "response": { - "isIncomplete": false, - "itemCount": 11, - "items": [ - { - "label": "id", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "score", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "score", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - }, - { - "label": "status", - "kind": 5, - "detail": "Field", - "labelDetails": { - "detail": " Field" - }, - "textEdit": { - "newText": "status", - "range": { - "end": { - "character": 72, - "line": 3 - }, - "start": { - "character": 72, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title", - "score", - "status" - ], - "forbiddenLabels": [] - }, - { - "probe": "create_arguments", - "analysisReadyMicros": 5181, - "firstRequestMicros": 480, - "warm": { - "samplesMicros": [ - 295, - 246 - ], - "p50Micros": 246, - "p95Micros": 295 - }, - "response": { - "isIncomplete": false, - "itemCount": 5, - "items": [ - { - "label": "title:", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - }, - { - "label": "status:", - "kind": 6, - "detail": "Status", - "labelDetails": { - "detail": " Status" - }, - "textEdit": { - "newText": "status: ", - "range": { - "end": { - "character": 70, - "line": 3 - }, - "start": { - "character": 70, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "title:", - "status:" - ], - "forbiddenLabels": [] - }, - { - "probe": "projected_record_fields", - "analysisReadyMicros": 7614, - "firstRequestMicros": 665, - "warm": { - "samplesMicros": [ - 556, - 553 - ], - "p50Micros": 553, - "p95Micros": 556 - }, - "response": { - "isIncomplete": false, - "itemCount": 6, - "items": [ - { - "label": "id", - "kind": 6, - "detail": "int", - "labelDetails": { - "detail": " int" - }, - "textEdit": { - "newText": "id", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - }, - { - "label": "title", - "kind": 6, - "detail": "String", - "labelDetails": { - "detail": " String" - }, - "textEdit": { - "newText": "title", - "range": { - "end": { - "character": 180, - "line": 3 - }, - "start": { - "character": 180, - "line": 3 - } - } - } - } - ] - }, - "requiredLabels": [ - "id", - "title" - ], - "forbiddenLabels": [ - "score", - "status" - ] - } - ], - "diagnostics": [ - { - "probe": "create_type", - "micros": 5178, - "version": 5, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 77 - }, - "end": { - "line": 3, - "character": 80 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'int' can't be assigned to the parameter type 'String'. ", - "range": { - "end": { - "character": 80, - "line": 3 - }, - "start": { - "character": 77, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - } - ] - }, - { - "probe": "predicate_type", - "micros": 6052, - "version": 6, - "expectedCode": "argument_type_not_assignable", - "expectedRange": { - "start": { - "line": 3, - "character": 81 - }, - "end": { - "line": 3, - "character": 88 - } - }, - "diagnostics": [ - { - "code": "argument_type_not_assignable", - "message": "The argument type 'String' can't be assigned to the parameter type 'int'. ", - "range": { - "end": { - "character": 88, - "line": 3 - }, - "start": { - "character": 81, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "selected_result", - "micros": 5228, - "version": 7, - "expectedCode": "invalid_assignment", - "expectedRange": { - "start": { - "line": 3, - "character": 124 - }, - "end": { - "line": 3, - "character": 134 - } - }, - "diagnostics": [ - { - "code": "invalid_assignment", - "message": "A value of type 'String' can't be assigned to a variable of type 'int'.\nTry changing the type of the variable, or casting the right-hand type to 'int'.", - "range": { - "end": { - "character": 134, - "line": 3 - }, - "start": { - "character": 124, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - }, - { - "probe": "missing_selected_field", - "micros": 6120, - "version": 8, - "expectedCode": "undefined_getter", - "expectedRange": { - "start": { - "line": 3, - "character": 169 - }, - "end": { - "line": 3, - "character": 174 - } - }, - "diagnostics": [ - { - "code": "undefined_getter", - "message": "The getter 'score' isn't defined for the type '({int id, String title})'.\nTry importing the library that defines 'score', correcting the name to the name of an existing getter, or defining a getter or field named 'score'.", - "range": { - "end": { - "character": 174, - "line": 3 - }, - "start": { - "character": 169, - "line": 3 - } - }, - "severity": 1, - "source": "dart" - }, - { - "code": "unused_import", - "message": "Unused import: 'models.dart'.\nTry removing the import directive.", - "range": { - "end": { - "character": 20, - "line": 2 - }, - "start": { - "character": 7, - "line": 2 - } - }, - "severity": 2, - "source": "dart" - } - ] - } - ], - "repairMicros": 4914, - "renameServerStartupMicros": 299474, - "symbolRenames": [ - { - "probe": "record_field", - "status": "unavailable", - "micros": 10700, - "prepare": null, - "error": null, - "workspaceEdit": null, - "applied": null - }, - { - "probe": "primary_constructor_field", - "status": "applied", - "micros": 1158280, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 53, - "line": 3 - }, - "start": { - "character": 49, - "line": 3 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 46, - "line": 12 - }, - "start": { - "character": 42, - "line": 12 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 1 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final name = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.name;\n" - } - } - }, - { - "probe": "table_field", - "status": "applied", - "micros": 24433, - "prepare": { - "placeholder": "name", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "displayName", - "range": { - "end": { - "character": 17, - "line": 7 - }, - "start": { - "character": 13, - "line": 7 - } - } - }, - { - "newText": "displayName", - "range": { - "end": { - "character": 54, - "line": 13 - }, - "start": { - "character": 50, - "line": 13 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 2 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 2, - "result": "import 'package:orm/orm.dart';\ntypedef RecordUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RecordUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRecordUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - }, - { - "probe": "record_alias", - "status": "applied", - "micros": 4755, - "prepare": { - "placeholder": "RecordUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - "error": null, - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 18, - "line": 1 - }, - "start": { - "character": 8, - "line": 1 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 29, - "line": 9 - }, - "start": { - "character": 19, - "line": 9 - } - } - }, - { - "newText": "RenamedUser", - "range": { - "end": { - "character": 10, - "line": 11 - }, - "start": { - "character": 0, - "line": 11 - } - } - } - ], - "textDocument": { - "uri": "fixture/lib/symbols.dart", - "version": 3 - } - } - ] - }, - "applied": { - "lib/symbols.dart": { - "edits": 3, - "result": "import 'package:orm/orm.dart';\ntypedef RenamedUser = ({int id, String name});\ntypedef OtherRecord = ({int id, String name});\nfinal class ClassUser(final int id, final String displayName);\nfinal class UserTable extends Fields {\n UserTable(super.table);\n late final id = column(Column('id', Codecs.integer));\n late final displayName = column(Column('name', Codecs.text));\n}\nString recordLabel(RenamedUser user) => user.name;\nString otherLabel(OtherRecord user) => user.name;\nRenamedUser recordValue() => (id: 1, name: 'first');\nString classLabel(ClassUser user) => user.displayName;\nExpr tableLabel(UserTable table) => table.displayName;\n" - } - } - } - ], - "ormRule": { - "dartAnalysisMilliseconds": 117, - "dartAnalysisOutput": "Analyzing schema.dart...\nNo issues found!\n\n", - "generatorError": "Key selectors accept direct fields or positional records of fields only. (offset 1644)" - }, - "finalAnalyzeMilliseconds": 461 - } - ] - }, - "limits": [ - "The 10-model same-session run failed before larger scales were attempted.", - "A server restart completes the same bounded rename probes. The underlying SDK/harness interaction is not diagnosed.", - "Record field rename is unavailable even in the clean session; use explicit edits, regeneration and analyzer-guided reference repair.", - "This is direct LSP evidence, not GUI or all-editor certification." - ] -} diff --git a/research/validation/editor-recovery.log b/research/validation/editor-recovery.log deleted file mode 100644 index 01dfc817..00000000 --- a/research/validation/editor-recovery.log +++ /dev/null @@ -1,23 +0,0 @@ -Running build hooks...Running build hooks...Editor protocol checks for 10 models... - completion: table_entries - completion: schema_record_fields - completion: query_fields - completion: create_arguments - completion: projected_record_fields - diagnostic: create_type - diagnostic: predicate_type - diagnostic: selected_result - diagnostic: missing_selected_field - requesting rename: record_field - rename: record_field (unavailable) - requesting rename: primary_constructor_field -Unhandled exception: -Bad state: Timed out: textDocument/rename {textDocument: {uri: file:///var/folders/gf/46z6v161517c7_h1zl31hj1m0000gn/T/orm-build-X8NKs5/lib/symbols.dart}, position: {line: 3, character: 49}, newName: displayName}; notifications=[{id: 30, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 31, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 32, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 33, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 34, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 35, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 36, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 37, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 38, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}, {id: 39, jsonrpc: 2.0, method: window/workDoneProgress/create, params: {token: ANALYZING}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: begin, title: Analyzing…}}}, {jsonrpc: 2.0, method: $/progress, params: {token: ANALYZING, value: {kind: end}}}]; stderr= -#0 EditorServer.request. (file:///Users/seven/workspace/dart-orm/tool/src/editor_server.dart:99:11) -#1 _RootZone.run (dart:async/zone.dart:1028:54) -#2 Future.timeout. (dart:async/future_impl.dart:1054:34) - -#3 EditorServer.request (file:///Users/seven/workspace/dart-orm/tool/src/editor_server.dart:96:14) - -#4 main (file:///Users/seven/workspace/dart-orm/tool/benchmark_editor.dart:264:22) - diff --git a/research/validation/flutter-android.png b/research/validation/flutter-android.png deleted file mode 100644 index b7421a4a..00000000 Binary files a/research/validation/flutter-android.png and /dev/null differ diff --git a/research/validation/flutter-web-probe.json b/research/validation/flutter-web-probe.json deleted file mode 100644 index 966d34fa..00000000 --- a/research/validation/flutter-web-probe.json +++ /dev/null @@ -1,500 +0,0 @@ -{ - "date": "2026-09-16", - "runtimeRevision": "a25c29c9a2e3025c5bb29fe3415c846e764d7373", - "scope": "Release Flutter builds with a temporary ORM package copy declaring web-only assets; shipping runtime unchanged.", - "standaloneDartPubGet": "passed offline; package keeps no Flutter SDK dependency", - "probeSha256": "9e99f722fcb0b11f5610d0441fe8baf236e616e326f897848b51c04587397648", - "artifacts": [ - { - "name": "sqlite3.wasm", - "bytes": 748686, - "gzipBytes": 358944, - "sha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "name": "worker.js", - "bytes": 175973, - "gzipBytes": 55940, - "sha256": "390820f8ceefca406e191ace4d594e06840808a7c09823ad031ccd5ebf893d44" - } - ], - "runs": [ - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 95 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 38 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 15 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 20 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 32 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 28 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 3 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 3 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 161 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 32 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 100 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 21 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": false, - "flutterFrames": 6, - "baseUri": "http://127.0.0.1:56191/nested/", - "dartUriBase": "http://127.0.0.1:56191/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/nested/main.dart.js", - "/favicon.ico", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/nested/main.dart.js", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/report" - ], - "build": "js", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 100 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 38 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 12 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 19 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 24 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 160 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 32 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 97 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 12 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 4, - "baseUri": "http://127.0.0.1:56219/nested/", - "dartUriBase": "http://127.0.0.1:56219/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/favicon.ico", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/report" - ], - "build": "wasm", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 56 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 41 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 12 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 18 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 23 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 160 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 31 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 96 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 14 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 4, - "baseUri": "http://127.0.0.1:56235/nested/", - "dartUriBase": "http://127.0.0.1:56235/nested/detail/42?phase=recover", - "isolated": true, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/favicon.ico", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/canvaskit/skwasm.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/web/worker.js", - "/nested/canvaskit/skwasm.js", - "/nested/assets/packages/orm/assets/web/sqlite3.wasm", - "/report" - ], - "build": "wasm", - "expectedIsolation": true, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - } - ] -} diff --git a/research/validation/flutter.json b/research/validation/flutter.json deleted file mode 100644 index 5919ce0a..00000000 --- a/research/validation/flutter.json +++ /dev/null @@ -1,292 +0,0 @@ -{ - "capturedAt": "2026-09-15T11:25:43.915364Z", - "runtimeSourceCommit": "85c4e42bf82924f806d1ce79ba96f1f61af61b1f", - "flutter": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "dartSdkVersion": "3.13.3" - }, - "device": "emulator-5580", - "abi": "arm64-v8a", - "buildFingerprint": "google/sdk_gphone64_arm64/emu64a:15/AE3A.240806.036/12592187:user/release-keys", - "apkSha256": { - "legacy": "3c949c03fc181f7dba9192175afc4a558a0e4eba071fc45453820ab0393fd2d6", - "current": "db5dbceb9f952a10a52f6d460e66b42fa9e41d05d421b04e329d3c30feec6767" - }, - "sourceSha256": { - "lib/builder.dart": "031bdbdaec51addad1eab835297e9bafe1daec68ca40658f02a53731c1d61c2e", - "lib/sqlite_web.dart": "7c0f9146a4fd851363ec66634e3ddd4199aec66da50017c6490cd0e47c269461", - "lib/orm.dart": "454b2f9030bc1b5f36caa3381cc5ba83be956a151c078b6be3659342a3570724", - "lib/generate.dart": "c8ea4b5ff50fb6a72fc28e27f3db07965afeab0d0228f67f784a01745b200af9", - "lib/sqlite_web_worker.dart": "c36fc527bbca1578338784b6dacee4489790d4eb980dd761e65ba2caba596a23", - "lib/migrate_cli.dart": "fdb509d4e6bccd5d6bb983d2a70c3f51da4aea3951c9e8bce56fa5266f742ff9", - "lib/sqlite.dart": "4190681a93d529cd181900d9c80236b66d784483a1d10a1d9f47ca9bfcb01957", - "lib/schema.dart": "85b88b9f54ecf5b6a0d2e4c772e839dad02d15532fa1db847be78b5e49b71d88", - "lib/postgres.dart": "b97492fe7b4004f3c996e20276448ec70cd147f059ada27901476a0b8251e5e7", - "lib/migrate.dart": "c3050956b5ad19b8b2ae32f47331ef2f3782b60e7dc9891d16be407943d43903", - "lib/src/instant.dart": "060bf3709d06e38a7483ce7c6f5a8b29239012b548136bcaffac1e554c00b518", - "lib/src/temporal.dart": "de55763e61ad70ea471763b01cd1a3fa5f0f5ed7c1e1416323e23dd8de83fa39", - "lib/src/mutation.dart": "e3af296711a57c43680d7b4660b9dd77230f77aa144ce4a9bbc5b4f3166ffb64", - "lib/src/union.dart": "3df7db11b8df824755ec2a8265381082b6fa36b99c093ed3d26c2c58fff6743c", - "lib/src/execution.dart": "9431cdc967f8ccee0ed9424fa872e05a965dff33c44b3be2435778f7252bfd37", - "lib/src/batch.dart": "675130ab5ffc1411cb3a5512ba7273fb0b917b1425ccbb0a9bf3f1faa699d929", - "lib/src/sql.dart": "4f7c1f39f666bd142bdb68eef08d9d0cd2f32807511d80d9f9150464697617bf", - "lib/src/watch.dart": "538ea16142701d135e95035c74a561d1179f5bf8e15a3ba07f80e8507ace3b70", - "lib/src/selection.dart": "038617d98192d42449deac8875af49b15412fa2915bc11569f1e33a29a46dd5f", - "lib/src/database.dart": "9fc5b2ee3753acb3b073cd1f00d1ecfea99ed12df10f1278fb3718be6940ce09", - "lib/src/decimal_average.dart": "05ed8eb87201087db3cc0bdc478fbb9d34054db5baa84c39b204cb7a45d5a117", - "lib/src/sqlite/failure.dart": "64bfb54aea1b7ef2cd7f7c65d06b85df3ef9a6cbc981f2546c5f55f071fea49b", - "lib/src/sqlite/functions.dart": "7ee775e738137457ebfdc2f911c320a5da24704d5e2aab651eabaf433f6646eb", - "lib/src/sqlite/temporal.dart": "0e874da5e0f6aa31d2d23db8edd458306d4e0dec2ce46397f471443186946243", - "lib/src/sqlite/web_wire.dart": "7b4460f2ad3cd922039fa2a50d3825a45dd9a65a7fa1adc114049b89658ae3ef", - "lib/src/sqlite/web_client.dart": "7a45b6affa89787cb8c8da5ca7a97abeb1b3540bd010606979197d1308e50db3", - "lib/src/sqlite/decimal.dart": "b291e5cfc0758fd944e7a09b345f8fa04b3e1679a099f92b9bf43023f57da5f0", - "lib/src/sqlite/worker.dart": "8f67a5a6be6adf3c5b35df7345ac8b01f61dcca6c151a62bd618497d85f5e870", - "lib/src/sqlite/native.dart": "36fe0a2ee78a9b740646b0244a6bf6eae276cfed29d890e799c0dc6b8c8e1830", - "lib/src/decimal_sql.dart": "60fbbea999e892f4166bf86052b73b028e9d68e4c134a0f92b170bd4b2760ed9", - "lib/src/table.dart": "e24ac71001350920de00c61c0134e79c0a955956d6a7950f632aaedf9aee5b68", - "lib/src/decimal.dart": "5693780043a8271953814861f235f3beef5c6ed64dd6d89f33784eccf2101357", - "lib/src/query.dart": "19bd107901c35a0fc80984e69712308899cba3097acb17a4bb29f74a75bdfdcb", - "lib/src/postgres/temporal.dart": "0dffac77da7fc1d744bf6d5c0735c653ba6a13ef451d01001b1135d78c2b90ef", - "lib/src/decimal_division.dart": "2fe5496e9c70bab94d05fdfa6317af6d862c2c8b0b75c175cfd510d82811e4f6", - "lib/src/migrate/sqlite_checks.dart": "fee6d23a9fae37b2208485fcf2ee24d7e104e65b546162833f12bfebf7457ee3", - "lib/src/migrate/recovery.dart": "8567928dbfea9d4b320957b5fb6f4c06aa834b882c9472024ab97c5d3286b5b3", - "lib/src/migrate/catalog.dart": "80c1a396727d57e9c49e37914c076e6fab64a3eb43437b522c82c749cf280359", - "lib/src/migrate/checks.dart": "def060b5110483eaccfd2f38eb59276eb8826f9bc37305574d0cf5f66e30c0b2", - "lib/src/migrate/schema.dart": "ecb46569f83192b4888f8af1aefd33a1d786c0e670d3f1eaeb264a59e4456e81", - "lib/src/migrate/backfill.dart": "2e02997c3cab9214167e25b3e830418384b1c704f534e5200a97519c995562f1", - "lib/src/migrate/snapshot.dart": "7a8d38fdd9d9078deb97fab50e851462c52cf8df606c65be387ff49e9792dfc3", - "lib/src/migrate/history.dart": "3d236ce64e8428e83692d2bce5f0b35a63401cbbba305d8a397bca402eb04563", - "lib/src/migrate/computed.dart": "cf34167f63590e36e72196ff5e6fac03fd9bb2d1dc28c7ef11b06d8ca9f943e8", - "lib/src/migrate/diff.dart": "de168f14e7b91e3cd350532eb5a0901f45310cd516b1f0aa1dd7f89d66a89d5c", - "lib/src/migrate/source.dart": "ee9637f5efa3a85a89573561027bc82930ddac14e5578742cdf1a27f609ceb7e", - "lib/src/migrate/step.dart": "750a02b0a4bfce6a6f3009eb6f72aa46813863da92493a6b23d38601c7d4f69f", - "lib/src/stream.dart": "ce6fd5da4a7dd17b5cdf09aae7eddc893c362c53251b0d627124419a7cc49e37", - "lib/src/generate/migrations.dart": "77ce5275428ea9efc3d0025d78a36607afda50ee83a5af5261e47ab73d263901", - "lib/src/generate/build.dart": "7020b5bf3b753c181c0efbf8afe5927b2f23b2d4c9f9362cfb864addf988083a", - "lib/src/generate/reader.dart": "d851c5d3d433a64a90a024047d64cedb8c8fa42585e49bd781bb3ab1232bcd71", - "lib/src/generate/import.dart": "7752c4e09f740e4cf8c4c61929306b6883ea853ee21fcde7e2427a5c180443a2", - "lib/src/generate/types.dart": "7fb4c9bea6762f9ee2dfa94f94419d883d52eb398c9346c1034b020c248fb86e", - "lib/src/generate/queries.dart": "85abf4adaeaaa7c2a5de47c6a9c953be79c2faf44f574f38a4f2d22004feef57", - "lib/src/generate/emitter.dart": "e676e662d2a2c8518e2f8a8df4045a159469ae4ed84c3df505301cfd04e21b99", - "lib/src/generate/model.dart": "1d0d28739ab18379278901632d55e7bb8b800b9898a8ffd64b58e6d133e5e516", - "lib/src/advanced.dart": "22de5b3bb320261282bca5c8446877e88129b34b68b839cfabf04dd8cbe783af", - "lib/src/named_sql.dart": "906b7bad1a499db8f582ef4e862d05ed5b8f4b08d24ddaaeff2b7555f1fb549d", - "lib/src/transaction.dart": "9e2da548a493b84c52d18acad9cfce65887c0b1c7f63615a2be5733d5ba68538", - "lib/src/relation.dart": "d78428fe2df25031d83500002b152853875f0762bf7875ed1f7725764c0b4acf", - "lib/src/cursor.dart": "3483a7b27b2f741a3782220cd7137c0fb7b6e0c9cf0254f4ddeb0068faf92852", - "lib/src/cte.dart": "e6241688c3648fff50476e3ffc517044ac9bc6c080ad6a2f56f482d4f415f610", - "lib/src/observation.dart": "e9471f15d1efdeb8233a50892b5c0cfeb0bd3b15b6a7790d4363551ae9473da7", - "lib/src/plan.dart": "9a58f35b3490ed2f13f55a1ec7c0c7a45117bf7265adec1bef9adab368d17e25", - "lib/src/codec.dart": "2f1d829d2cf043e76ac123eea6554206d82de0cbcdf8c34848075f21fe6d4765", - "example/flutter/lib/migrations/migrations.g.dart": "74eb85031b3aa459c5c13768926610b4bd9f2c251d7a9d1c6398144ed0531539", - "example/flutter/lib/migrations/m0002_comments.dart": "5fdbdc8ec612618de36649a66b331fa006a17a6d0787a597740d159c2e1cc870", - "example/flutter/lib/migrations/m0001_initial.dart": "71d878de428f54afb0e45f949149c61c0bed8462dbfbdba1056bffa0573b17fe", - "example/flutter/lib/schema.orm.dart": "80f1d1e1f3b21f0882671c58d9e3e2e9a840b8f077af10dc40a1c217feb9c7f5", - "example/flutter/lib/schema.snapshot.dart": "8bb4b6a07f2e2f8c3b5123a2c5e325870ed4baca817ae1a97365f14d1731c58f", - "example/flutter/lib/legacy/seed.dart": "a70c0c5191f9fd6632bb9bcd36a58649d486cd183b9e32ea8ce42238b9e27f3c", - "example/flutter/lib/legacy/schema.orm.dart": "600a281687d9f11ae299de8ecfd79cff1ff243f0343136256a8d263463b57ef9", - "example/flutter/lib/legacy/schema.snapshot.dart": "2f74337b040f831ecaac3d9268011bb27af2cf6728bb84c427ce7d95d14a713e", - "example/flutter/lib/legacy/schema.dart": "7bdd9b8f8cb97747d632a6da573883cde5fc5cfb60ab1ae2a6bf3da2df36f93a", - "example/flutter/lib/schema.dart": "ee6e7c93f615fcdcd60a020339bbfc0099ab48e0128d12ba20f362685802e0d6", - "example/flutter/lib/main.dart": "53b31b2eebcf8faa2eb25a8c6563619b069408e120d1b13c44967fb4053df287", - "example/flutter/lib/acceptance.dart": "4acdc0a8b500d9be09dab2cd02e3725da838d00f8131155d549dc0a4b7458420", - "example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png": "c7c0c0189145e4e32a401c61c9bdc615754b0264e7afae24e834bb81049eaf81", - "example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png": "6a7c8f0d703e3682108f9662f813302236240d3f8f638bb391e32bfb96055fef", - "example/flutter/android/app/src/main/res/drawable/launch_background.xml": "94433756fe10005ddd6fc55cc5c0b1e9f3c10b66a08ceac795ece6713d34e28a", - "example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": "3c34e1f298d0c9ea3455d46db6b7759c8211a49e9ec6e44b635fc5c87dfb4180", - "example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png": "4d470bf22d5c17d84edc5f82516d1ba8a1c09559cd761cefb792f86d9f52b540", - "example/flutter/android/app/src/main/res/values-night/styles.xml": "8c5bad5152f217e837519f9c15211d19f39bd45f5f67faf6dd81cb4b299e518f", - "example/flutter/android/app/src/main/res/values/styles.xml": "937d7c82f539c72eb94fd3a3773f29b3d3fef1997213c92cb632298cedf4bda9", - "example/flutter/android/app/src/main/res/drawable-v21/launch_background.xml": "7ca6d5cec2169c5a98661a119db23a6f3399c8fef2256ff04f3a642831282478", - "example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png": "e14aa40904929bf313fded22cf7e7ffcbf1d1aac4263b5ef1be8bfce650397aa", - "example/flutter/android/app/src/main/AndroidManifest.xml": "6bc850ca03629a837d3dc7b82ba7cbe6c2b969d69ff49a6bea814a651d473c76", - "example/flutter/android/app/src/main/kotlin/dev/orm/orm_flutter/MainActivity.kt": "2c1111a536c7554c4e39eb96dbf1f0a136c51f0dc3a5d9b87c7545f2272a77d8", - "tool/test_flutter.dart": "ce813e2715977ed4202ea7696df7be3789e894dbb290035b9db64052276278f0", - "example/flutter/pubspec.yaml": "8484b780de943ed56d9727cb7db47b75f372efed4f7b48e652fdcdfa911ae286", - "example/flutter/pubspec.lock": "4773a8bd30cbebbc25ed26c51dea715f6018d7e320f4e9a24fcdb93874ae1318", - "example/flutter/.metadata": "ac4f09e51478c44ffef6b566e3616f16a8b21db8b17eb51969b03cb2e1a2842e", - "example/flutter/android/app/build.gradle.kts": "67cc1825bb2528f74eb6d8577f99f22dd9c23fc8bb61d9be9a80d80e7fa9ecfb", - "example/flutter/android/settings.gradle.kts": "016759283e88ade6eba92910583feeef88b962839b957aa414296afd2952d122", - "example/flutter/android/gradle/wrapper/gradle-wrapper.properties": "b690d26223576fe4e63889fad9a00df81945cb870b9476cc5a563152a1a88a74" - }, - "screenshotSha256": "93f445c4187017a497f54c34bfe5ff74014834db28024146e8f867896a9cd054", - "phases": [ - { - "phase": "legacy", - "schemaVersion": 1, - "release": false, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 6123, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Fresh database", - "Version 1 typed transaction persisted two notes", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "legacyRows": [ - [ - 1, - "Written by version 1", - "2026-09-15 01:02:03.123456+00" - ], - [ - 2, - "Keep this across the upgrade", - "2026-09-15 00:00:00.000000+00" - ] - ], - "history": [ - { - "id": "0001_initial", - "checksum": "a5d45b89caaf4503d516f6b43138c4fd40e6ca88114f88ac1c3997af1afef421" - } - ], - "status": "passed", - "apkVersionCode": 1 - }, - { - "phase": "upgrade", - "schemaVersion": 2, - "release": true, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 6324, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Existing database retained", - "Known execution phase", - "Expected previous migration history", - "Only version 2 migration applied", - "Live schema matches bundled snapshot", - "Original value and timestamp microseconds preserved", - "New field default / persisted update", - "Initial watch snapshot delivered to Flutter", - "Committed relation write refreshed watch", - "Rollback neither persisted nor notified", - "Typed patch refreshed watch", - "Background SQLite computation result", - "Dart timer and Flutter frames advanced during SQLite work", - "Native SQLite cancellation", - "Worker usable after cancellation", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "unmanaged": [], - "background": { - "elapsedUs": 335844, - "timerTicks": 20, - "flutterFrames": 12 - }, - "watchSnapshots": 3, - "rows": [ - { - "id": 1, - "text": "Written by version 1", - "done": true, - "comments": [] - }, - { - "id": 2, - "text": "Keep this across the upgrade", - "done": false, - "comments": [] - }, - { - "id": 3, - "text": "Written by version 2", - "done": false, - "comments": [ - "Related in one transaction" - ] - } - ], - "history": [ - { - "id": "0001_initial", - "checksum": "a5d45b89caaf4503d516f6b43138c4fd40e6ca88114f88ac1c3997af1afef421" - }, - { - "id": "0002_comments", - "checksum": "089cd5d7ab42fa7491b8e46ca3b00899b34f926983f6a93f2812cf1b0c9afb92" - } - ], - "status": "passed", - "apkVersionCode": 2 - }, - { - "phase": "reopen", - "schemaVersion": 2, - "release": true, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 6401, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Existing database retained", - "Known execution phase", - "Expected previous migration history", - "No migration replay", - "Live schema matches bundled snapshot", - "Original value and timestamp microseconds preserved", - "New field default / persisted update", - "Initial watch snapshot delivered to Flutter", - "Relation data survived process restart", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "unmanaged": [], - "watchSnapshots": 1, - "rows": [ - { - "id": 1, - "text": "Written by version 1", - "done": true, - "comments": [] - }, - { - "id": 2, - "text": "Keep this across the upgrade", - "done": false, - "comments": [] - }, - { - "id": 3, - "text": "Written by version 2", - "done": false, - "comments": [ - "Related in one transaction" - ] - } - ], - "history": [ - { - "id": "0001_initial", - "checksum": "a5d45b89caaf4503d516f6b43138c4fd40e6ca88114f88ac1c3997af1afef421" - }, - { - "id": "0002_comments", - "checksum": "089cd5d7ab42fa7491b8e46ca3b00899b34f926983f6a93f2812cf1b0c9afb92" - } - ], - "status": "passed", - "apkVersionCode": 2 - } - ], - "limits": [ - "Android arm64 API 35 emulator; not physical hardware, iOS or macOS Flutter.", - "Legacy APK debug; upgraded/restarted APK release AOT.", - "Frame and timer progress demonstrate worker separation, not an FPS benchmark.", - "Force-stop/relaunch is process persistence evidence, not power-loss testing." - ] -} diff --git a/research/validation/migration-targets-backfill-aot.log b/research/validation/migration-targets-backfill-aot.log deleted file mode 100644 index faa73818..00000000 --- a/research/validation/migration-targets-backfill-aot.log +++ /dev/null @@ -1,4 +0,0 @@ -Running build hooks...Running build hooks...Running link hooks...Running link hooks...Copying 1 build assets: -package:sqlite3/src/ffi/libsqlite3.g.dart -Generated: /Users/seven/workspace/dart-orm/.dart_tool/target-backfill-aot/bundle/bin/native_backfill -Native AOT: historical backfill declaration, bounded pause, concurrent resume and startup compatibility passed. diff --git a/research/validation/migration-targets-browser-js.log b/research/validation/migration-targets-browser-js.log deleted file mode 100644 index e4e7ece0..00000000 --- a/research/validation/migration-targets-browser-js.log +++ /dev/null @@ -1,86 +0,0 @@ -Running build hooks...Running build hooks...{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 66 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 37 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 15 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 33 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 25 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 3 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 2 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 3 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 4 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 4 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 159 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 92 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 3 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 20 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "js", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/migration-targets-browser-wasm.log b/research/validation/migration-targets-browser-wasm.log deleted file mode 100644 index d1226e7b..00000000 --- a/research/validation/migration-targets-browser-wasm.log +++ /dev/null @@ -1,86 +0,0 @@ -Running build hooks...Running build hooks...{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 51 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 39 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 13 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 25 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 22 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 2 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 157 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 29 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 87 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 13 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "wasm", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/migration-targets-cli.log b/research/validation/migration-targets-cli.log deleted file mode 100644 index b844bcb0..00000000 --- a/research/validation/migration-targets-cli.log +++ /dev/null @@ -1,5 +0,0 @@ -CLI registry initialization, regeneration and rejected retarget: passed -{"command": "check", "valid": true, "dialect": "sqlite", "migrations": ["0001_initial"]} -{"command": "apply", "applied": ["0001_initial"]} -{"command": "verify", "matches": true, "differences": [], "unmanaged": []} -{"command": "apply", "applied": []} diff --git a/research/validation/migration-targets-followup.log b/research/validation/migration-targets-followup.log deleted file mode 100644 index ec7ffcfb..00000000 --- a/research/validation/migration-targets-followup.log +++ /dev/null @@ -1,32 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/migration_target_test.dart -00:00 +0: test/migration_target_test.dart: history requires a single engine even when empty or SQL is portable -00:00 +1: test/migration_target_test.dart: unselected expressions cannot change a saved plan, hash or source -00:00 +2: test/migration_target_test.dart: SQLite virtual indexes and computed mode changes ignore PostgreSQL limits -00:00 +3: test/migration_target_test.dart: replacing stored values with computed values requires explicit review -00:00 +4: test/migration_target_test.dart: older PostgreSQL is rejected before locks, journals or partial execution -00:00 +5: test/migration_target_test.dart: registry fixes its engine at initialization and refuses retargeting -00:00 +6: test/migration_target_test.dart: wrong engine fails before any migration SQL for runner and every connected CLI command -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -00:00 +7: test/migration_target_test.dart: PostgreSQL applies only its own generated conversion and rejects a SQLite history before SQL -00:00 +8: loading test/migration_recovery_test.dart -00:00 +8: test/migration_recovery_test.dart: (setUpAll) -00:00 +8: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:00 +9: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:00 +10: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:00 +11: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:00 +12: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:00 +13: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:00 +14: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:00 +15: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:01 +16: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:01 +17: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:01 +18: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:01 +19: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -00:11 +20: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -00:19 +21: test/migration_recovery_test.dart: (tearDownAll) -00:19 +21: All tests passed! diff --git a/research/validation/migration-targets-full.log b/research/validation/migration-targets-full.log deleted file mode 100644 index 39540cd4..00000000 --- a/research/validation/migration-targets-full.log +++ /dev/null @@ -1,948 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/client_default_test.dart -00:00 +0: test/client_default_test.dart: client defaults sqlite omitted values use typed Dart factories and return stored records -00:00 +1: test/client_default_test.dart: client defaults sqlite explicit values, null and SQL DEFAULT bypass their client factory -00:00 +2: test/client_default_test.dart: client defaults sqlite prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +3: test/client_default_test.dart: client defaults sqlite batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +4: test/client_default_test.dart: client defaults sqlite updates and conflict updates do not reset omitted client values -00:00 +5: test/client_default_test.dart: client defaults sqlite factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +6: test/client_default_test.dart: client defaults sqlite snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +7: test/client_default_test.dart: client defaults sqlite explicit DEFAULT can choose database identity instead of the client value -00:00 +8: test/client_default_test.dart: client defaults postgres omitted values use typed Dart factories and return stored records -00:00 +9: test/client_default_test.dart: client defaults postgres explicit values, null and SQL DEFAULT bypass their client factory -00:00 +10: test/client_default_test.dart: client defaults postgres prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +11: test/client_default_test.dart: client defaults postgres batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +12: test/client_default_test.dart: client defaults postgres updates and conflict updates do not reset omitted client values -00:00 +13: test/client_default_test.dart: client defaults postgres factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +14: test/client_default_test.dart: client defaults postgres snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +15: test/client_default_test.dart: client defaults postgres explicit DEFAULT can choose database identity instead of the client value -00:00 +16: test/client_default_test.dart: generation reads factory references without executing them and supports moved output -00:02 +17: loading test/decimal_precision_test.dart -00:02 +17: test/decimal_precision_test.dart: decimal precision sqlite column coercion rounds signed ties and handles negative and excess scales -00:02 +18: test/decimal_precision_test.dart: decimal precision sqlite writes constrain columns while arithmetic and wider aggregates retain their result precision -00:02 +19: test/decimal_precision_test.dart: decimal precision sqlite batch inserts and upserts use column coercion and roll back as a unit -00:02 +20: test/decimal_precision_test.dart: decimal precision sqlite catalogs snapshots and imported declarations retain precision scale and defaults -00:04 +21: test/decimal_precision_test.dart: decimal precision sqlite reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:04 +22: test/decimal_precision_test.dart: decimal precision sqlite physical renames and changing constrained defaults preserve column contracts -00:04 +23: test/decimal_precision_test.dart: decimal precision sqlite maximum declared precision and scale bounds remain exact -00:04 +24: test/decimal_precision_test.dart: decimal precision sqlite historical backfills retain constrained decimal key declarations -00:04 +25: test/decimal_precision_test.dart: decimal precision postgres column coercion rounds signed ties and handles negative and excess scales -00:04 +26: test/decimal_precision_test.dart: decimal precision postgres writes constrain columns while arithmetic and wider aggregates retain their result precision -00:04 +27: test/decimal_precision_test.dart: decimal precision postgres batch inserts and upserts use column coercion and roll back as a unit -00:04 +28: test/decimal_precision_test.dart: decimal precision postgres catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +29: test/decimal_precision_test.dart: decimal precision postgres reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:05 +30: test/decimal_precision_test.dart: decimal precision postgres physical renames and changing constrained defaults preserve column contracts -00:05 +31: test/decimal_precision_test.dart: decimal precision postgres maximum declared precision and scale bounds remain exact -00:05 +32: test/decimal_precision_test.dart: decimal precision postgres historical backfills retain constrained decimal key declarations -00:05 +33: test/decimal_precision_test.dart: SQLite enforces stored precision and checks default coercion and trusted-schema behavior -00:05 +34: test/decimal_precision_test.dart: generation validates DecimalDigits and retains custom decimal codec types -00:07 +35: test/decimal_precision_test.dart: metadata rejects invalid precisions scales and non-decimal columns -00:07 +36: loading test/selection_test.dart -00:07 +36: test/selection_test.dart: selection sqlite all typed arities defer mapping and preserve left-to-right evaluation -00:07 +37: test/selection_test.dart: selection sqlite first decoder failure prevents later fields and outer mapper -00:07 +38: test/selection_test.dart: selection sqlite dynamic fields snapshot keys and preserve nulls and mapping order -00:07 +39: test/selection_test.dart: selection postgres all typed arities defer mapping and preserve left-to-right evaluation -00:07 +40: test/selection_test.dart: selection postgres first decoder failure prevents later fields and outer mapper -00:07 +41: test/selection_test.dart: selection postgres dynamic fields snapshot keys and preserve nulls and mapping order -00:07 +42: loading test/build_runner_test.dart -00:07 +42: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -00:37 +43: loading test/schema_version_test.dart -00:37 +43: test/schema_version_test.dart: schema version sqlite invalid ranges fail before SQL; caller transactions stay usable -00:37 +44: test/schema_version_test.dart: schema version sqlite unversioned existing data is rejected without creating history -00:37 +45: test/schema_version_test.dart: schema version sqlite default requires latest; inclusive compatibility range is explicit -00:37 +46: test/schema_version_test.dart: schema version sqlite newer database rejects downgrade without changing data or schema -00:37 +47: test/schema_version_test.dart: schema version sqlite complete applied prefix is checked, including old checksums and missing entries -00:37 +48: test/schema_version_test.dart: schema version sqlite matching version does not claim that the catalog is free of drift -00:37 +49: test/schema_version_test.dart: schema version sqlite verified baseline is compatible without replaying creation -00:37 +50: test/schema_version_test.dart: schema version sqlite borrowed session uses a read snapshot and survives rejected versions -00:37 +51: test/schema_version_test.dart: schema version postgres invalid ranges fail before SQL; caller transactions stay usable -00:37 +52: test/schema_version_test.dart: schema version postgres unversioned existing data is rejected without creating history -00:37 +53: test/schema_version_test.dart: schema version postgres default requires latest; inclusive compatibility range is explicit -00:37 +54: test/schema_version_test.dart: schema version postgres newer database rejects downgrade without changing data or schema -00:37 +55: test/schema_version_test.dart: schema version postgres complete applied prefix is checked, including old checksums and missing entries -00:37 +56: test/schema_version_test.dart: schema version postgres matching version does not claim that the catalog is free of drift -00:37 +57: test/schema_version_test.dart: schema version postgres verified baseline is compatible without replaying creation -00:37 +58: test/schema_version_test.dart: schema version postgres borrowed session uses a read snapshot and survives rejected versions -00:37 +59: test/schema_version_test.dart: schema version postgres unfinished recoverable migration blocks startup until repaired and completed -00:37 +60: test/schema_version_test.dart: schema version postgres catalog and history share a snapshot and the supplied history is frozen -00:38 +61: test/schema_version_test.dart: SQLite version gate works on an explicitly read-only file -00:38 +62: loading test/types_test.dart -00:38 +62: test/types_test.dart: junction-table keys, enum payloads and nested results keep generated types -00:39 +63: test/types_test.dart: computed generated fields are statically readable and never writable -00:40 +64: test/types_test.dart: client-default create parameters preserve domain, nullable and timestamp types -00:40 +65: test/types_test.dart: query-only relation APIs retain result types and expose no graph writes -00:41 +66: test/types_test.dart: calendar APIs reject instants, strings and mixed temporal types -00:41 +67: test/types_test.dart: actual generated APIs reject wrong inputs, results and backend options -00:41 +68: loading test/migration_test.dart -00:41 +68: test/migration_test.dart: (setUpAll) -00:42 +68: test/migration_test.dart: migrations sqlite explicit table and column renames preserve incoming references and data -00:42 +69: test/migration_test.dart: migrations sqlite constraint changes resolve catalog names and retain rows -00:42 +70: test/migration_test.dart: migrations sqlite failed NOT NULL rebuild restores data history and foreign key settings -00:42 +71: test/migration_test.dart: migrations sqlite reviewed conversion expressions preserve values when storage types change -00:42 +72: test/migration_test.dart: migrations sqlite additive changes use ALTER without a table rebuild -00:42 +73: test/migration_test.dart: migrations sqlite reviewed table removal handles restrictive foreign keys atomically -00:42 +74: test/migration_test.dart: migrations sqlite broken migration chain is refused before DDL -00:42 +75: test/migration_test.dart: migrations sqlite catalog distinguishes nullable non-rowid primary keys -00:42 +76: test/migration_test.dart: migrations sqlite rebuild preserves unmanaged index trigger and view definitions -00:42 +77: test/migration_test.dart: migrations sqlite foreign key check rolls back changed parent keys without cascading data away -00:42 +78: test/migration_test.dart: migrations sqlite unmodeled CHECK constraints are refused without losing their definition -00:42 +79: test/migration_test.dart: migrations postgres explicit table and column renames preserve incoming references and data -00:42 +80: test/migration_test.dart: migrations postgres constraint changes resolve catalog names and retain rows -00:42 +81: test/migration_test.dart: migrations postgres failed NOT NULL rebuild restores data history and foreign key settings -00:42 +82: test/migration_test.dart: migrations postgres reviewed conversion expressions preserve values when storage types change -00:42 +83: test/migration_test.dart: migrations postgres additive changes use ALTER without a table rebuild -00:42 +84: test/migration_test.dart: migrations postgres reviewed table removal handles restrictive foreign keys atomically -00:42 +85: test/migration_test.dart: migrations postgres broken migration chain is refused before DDL -00:42 +86: test/migration_test.dart: diff requires explicit renames, casts and backfills -00:42 +87: test/migration_test.dart: (tearDownAll) -00:42 +87: loading test/many_to_many_test.dart -00:42 +87: test/many_to_many_test.dart: many-to-many sqlite two declared FKs and a composite primary key enforce association identity -00:42 +88: test/many_to_many_test.dart: many-to-many sqlite forward projection keeps payload, shared targets and per-user limits in two statements -00:42 +89: test/many_to_many_test.dart: many-to-many sqlite reverse projection applies tie-breaking and offset to each team without changing root pagination -00:42 +90: test/many_to_many_test.dart: many-to-many sqlite nested team rosters deduplicate shared team keys and batch by level -00:42 +91: test/many_to_many_test.dart: many-to-many sqlite association predicates and counts stay in one SQL statement -00:42 +92: test/many_to_many_test.dart: many-to-many sqlite limited parameter capacity chunks associations without changing per-parent limits -00:42 +93: test/many_to_many_test.dart: many-to-many sqlite transaction rollback restores payload changes and new endpoints after an invalid association -00:42 +94: test/many_to_many_test.dart: many-to-many sqlite composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:42 +95: test/many_to_many_test.dart: many-to-many sqlite watch tracks junction payload and opposite endpoint changes after commit -00:42 +96: test/many_to_many_test.dart: many-to-many postgres two declared FKs and a composite primary key enforce association identity -00:42 +97: test/many_to_many_test.dart: many-to-many postgres forward projection keeps payload, shared targets and per-user limits in two statements -00:42 +98: test/many_to_many_test.dart: many-to-many postgres reverse projection applies tie-breaking and offset to each team without changing root pagination -00:42 +99: test/many_to_many_test.dart: many-to-many postgres nested team rosters deduplicate shared team keys and batch by level -00:42 +100: test/many_to_many_test.dart: many-to-many postgres association predicates and counts stay in one SQL statement -00:42 +101: test/many_to_many_test.dart: many-to-many postgres limited parameter capacity chunks associations without changing per-parent limits -00:42 +102: test/many_to_many_test.dart: many-to-many postgres transaction rollback restores payload changes and new endpoints after an invalid association -00:42 +103: test/many_to_many_test.dart: many-to-many postgres composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:43 +104: test/many_to_many_test.dart: many-to-many postgres watch tracks junction payload and opposite endpoint changes after commit -00:43 +105: loading test/migration_target_test.dart -00:43 +105: test/migration_target_test.dart: history requires a single engine even when empty or SQL is portable -00:43 +106: test/migration_target_test.dart: unselected expressions cannot change a saved plan, hash or source -00:43 +107: test/migration_target_test.dart: SQLite virtual indexes and computed mode changes ignore PostgreSQL limits -00:43 +108: test/migration_target_test.dart: replacing stored values with computed values requires explicit review -00:43 +109: test/migration_target_test.dart: older PostgreSQL is rejected before locks, journals or partial execution -00:43 +110: test/migration_target_test.dart: registry fixes its engine at initialization and refuses retargeting -00:43 +111: test/migration_target_test.dart: wrong engine fails before any migration SQL for runner and every connected CLI command -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -00:43 +112: test/migration_target_test.dart: PostgreSQL applies only its own generated conversion and rejects a SQLite history before SQL -00:43 +113: loading test/stream_test.dart -00:43 +113: test/stream_test.dart: sqlite cancellation between batch chunks prevents a partial commit -00:43 +114: test/stream_test.dart: sqlite cancellation interrupts an active cursor fetch -00:43 +115: test/stream_test.dart: sqlite timed-out mutations return no partial write -00:43 +116: test/stream_test.dart: sqlite database close stops a paused cursor and releases the driver -00:43 +117: test/stream_test.dart: sqlite cancellation between stream creation and listening needs no rollback -00:43 +118: test/stream_test.dart: sqlite bounded cursor fetches preserve parameters and typed rows -00:43 +119: test/stream_test.dart: sqlite pause stops fetches and early cancellation releases the lease -00:43 +120: test/stream_test.dart: sqlite await-for provides demand and take closes without exhausting -00:43 +121: test/stream_test.dart: sqlite relation projections load once per root batch -00:43 +122: test/stream_test.dart: sqlite mapper failure closes the cursor and rolls back -00:43 +123: test/stream_test.dart: sqlite early stream end inside a transaction preserves other work -00:43 +124: test/stream_test.dart: sqlite escaping paused stream cannot keep a transaction alive -00:43 +125: test/stream_test.dart: sqlite cancels executing SQL and allows the next statement -00:43 +126: test/stream_test.dart: sqlite statement timeout stops SQL and leaves a usable connection -00:43 +127: test/stream_test.dart: sqlite cancelled statement poisons a transaction even if caught -00:43 +128: test/stream_test.dart: sqlite cancelling a paused stream releases its connection before resume -00:43 +129: test/stream_test.dart: sqlite cancellation before starting performs no SQL -00:43 +130: test/stream_test.dart: postgres cancellation between batch chunks prevents a partial commit -00:43 +131: test/stream_test.dart: postgres cancellation interrupts an active cursor fetch -00:43 +132: test/stream_test.dart: postgres timed-out mutations return no partial write -00:44 +133: test/stream_test.dart: postgres database close stops a paused cursor and releases the driver -00:44 +134: test/stream_test.dart: postgres cancellation between stream creation and listening needs no rollback -00:44 +135: test/stream_test.dart: postgres bounded cursor fetches preserve parameters and typed rows -00:44 +136: test/stream_test.dart: postgres pause stops fetches and early cancellation releases the lease -00:44 +137: test/stream_test.dart: postgres await-for provides demand and take closes without exhausting -00:44 +138: test/stream_test.dart: postgres relation projections load once per root batch -00:44 +139: test/stream_test.dart: postgres mapper failure closes the cursor and rolls back -00:44 +140: test/stream_test.dart: postgres early stream end inside a transaction preserves other work -00:44 +141: test/stream_test.dart: postgres escaping paused stream cannot keep a transaction alive -00:44 +142: test/stream_test.dart: postgres cancels executing SQL and allows the next statement -00:44 +143: test/stream_test.dart: postgres statement timeout stops SQL and leaves a usable connection -00:44 +144: test/stream_test.dart: postgres cancelled statement poisons a transaction even if caught -00:44 +145: test/stream_test.dart: postgres cancelling a paused stream releases its connection before resume -00:44 +146: test/stream_test.dart: postgres cancellation before starting performs no SQL -00:44 +147: test/stream_test.dart: late PostgreSQL cancellation cannot reach the next statement -00:44 +148: test/stream_test.dart: borrowed pool requires cancellation support and retains ownership -00:44 +149: test/stream_test.dart: failed cancellation control discards the physical connection -00:44 +150: loading test/migration_recovery_test.dart -00:45 +150: test/migration_recovery_test.dart: (setUpAll) -00:45 +150: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:45 +151: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:54 +152: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:55 +153: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:55 +154: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:55 +155: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:55 +156: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:55 +157: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:55 +158: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:55 +159: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:55 +160: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:55 +161: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -01:00 +161 -1: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay [E] - Expected: <91> - Actual: <255> - - Running build hooks...Running build hooks...Unhandled exception: - OrmException(MIGRATION.TARGET): 0001_initial targets postgres, but this history requires sqlite. - #0 validateMigrations (package:orm/src/migrate/history.dart:362:7) - #1 MigrationHistory.checked (package:orm/src/migrate/source.dart:24:5) - #2 main (file:///private/var/folders/gf/46z6v161517c7_h1zl31hj1m0000gn/T/orm-build-h4pesJ/bin/crash.dart:3:73) - #3 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:311:33) - #4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12) - - - package:matcher expect - test/migration_recovery_test.dart 417:11 main. - -01:00 +161 -1: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -01:08 +161 -2: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay [E] - Expected: <91> - Actual: <255> - - Running build hooks...Running build hooks...Unhandled exception: - OrmException(MIGRATION.TARGET): 0001_initial targets postgres, but this history requires sqlite. - #0 validateMigrations (package:orm/src/migrate/history.dart:362:7) - #1 MigrationHistory.checked (package:orm/src/migrate/source.dart:24:5) - #2 main (file:///private/var/folders/gf/46z6v161517c7_h1zl31hj1m0000gn/T/orm-build-blAlWv/bin/crash.dart:3:73) - #3 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:311:33) - #4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12) - - - package:matcher expect - test/migration_recovery_test.dart 417:11 main. - -01:08 +161 -2: test/migration_recovery_test.dart: (tearDownAll) -01:08 +161 -2: loading test/acquisition_test.dart -01:08 +161 -2: test/acquisition_test.dart: an expired lease cannot enter before a delayed Timer gets its turn -01:08 +162 -2: test/acquisition_test.dart: acquisition sqlite expired raw writes never execute after a late lease becomes available -01:08 +163 -2: test/acquisition_test.dart: acquisition sqlite query cancellation returns while the pool is still occupied -01:08 +164 -2: test/acquisition_test.dart: acquisition sqlite typed mutations and batch transactions propagate acquisition limits -01:08 +165 -2: test/acquisition_test.dart: acquisition sqlite expired transactions and sessions never enter their callbacks -01:08 +166 -2: test/acquisition_test.dart: acquisition sqlite acquisition timeout stops on entry and statement timeout starts afterward -01:08 +167 -2: test/acquisition_test.dart: acquisition sqlite invalid and already-cancelled acquisition options never queue work -01:08 +168 -2: test/acquisition_test.dart: acquisition sqlite stream cancellation and timeout return without opening a cursor -01:08 +169 -2: test/acquisition_test.dart: acquisition sqlite query watch acquisition failure can recover on a later invalidation -01:08 +170 -2: test/acquisition_test.dart: acquisition sqlite close drains abandoned reservations without running their callbacks -01:08 +171 -2: test/acquisition_test.dart: acquisition sqlite abandoned requests preserve pool reuse and subsequent action errors -01:08 +172 -2: test/acquisition_test.dart: PostgreSQL native pool timeout is classified before entering the callback -01:09 +173 -2: test/acquisition_test.dart: PostgreSQL connection establishment timeout does not cap pool waiting -01:09 +174 -2: test/acquisition_test.dart: acquisition postgres expired raw writes never execute after a late lease becomes available -01:09 +175 -2: test/acquisition_test.dart: acquisition postgres query cancellation returns while the pool is still occupied -01:09 +176 -2: test/acquisition_test.dart: acquisition postgres typed mutations and batch transactions propagate acquisition limits -01:09 +177 -2: test/acquisition_test.dart: acquisition postgres expired transactions and sessions never enter their callbacks -01:09 +178 -2: test/acquisition_test.dart: acquisition postgres acquisition timeout stops on entry and statement timeout starts afterward -01:09 +179 -2: test/acquisition_test.dart: acquisition postgres invalid and already-cancelled acquisition options never queue work -01:09 +180 -2: test/acquisition_test.dart: acquisition postgres stream cancellation and timeout return without opening a cursor -01:09 +181 -2: test/acquisition_test.dart: acquisition postgres query watch acquisition failure can recover on a later invalidation -01:10 +182 -2: test/acquisition_test.dart: acquisition postgres close drains abandoned reservations without running their callbacks -01:10 +183 -2: test/acquisition_test.dart: acquisition postgres abandoned requests preserve pool reuse and subsequent action errors -01:10 +184 -2: test/acquisition_test.dart: PostgreSQL timeout during real connection initialization releases the late lease -01:10 +185 -2: loading test/computed_test.dart -01:10 +185 -2: test/computed_test.dart: computed sqlite native expressions preserve quoted identifiers, comments and SQL-like string values -01:10 +186 -2: test/computed_test.dart: computed sqlite generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:10 +187 -2: test/computed_test.dart: computed sqlite catalog/import roundtrip preserves SQL and modes and detects drift -01:12 +188 -2: test/computed_test.dart: computed sqlite ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:12 +189 -2: test/computed_test.dart: computed sqlite ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:12 +190 -2: test/computed_test.dart: computed sqlite stored to ordinary materialization retains values and permits later writes -01:12 +191 -2: test/computed_test.dart: computed sqlite explicit renamed expressions preserve indexes, checks, references and views -01:12 +192 -2: test/computed_test.dart: computed sqlite invalid recomputation rolls back the table and migration history -01:12 +193 -2: test/computed_test.dart: computed sqlite decimal computed SQL keeps precision coercion and imports exactly once -01:13 +194 -2: test/computed_test.dart: computed postgres native expressions preserve quoted identifiers, comments and SQL-like string values -01:13 +195 -2: test/computed_test.dart: computed postgres generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:13 +196 -2: test/computed_test.dart: computed postgres catalog/import roundtrip preserves SQL and modes and detects drift -01:13 +197 -2: test/computed_test.dart: computed postgres ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:13 +198 -2: test/computed_test.dart: computed postgres ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:13 +199 -2: test/computed_test.dart: computed postgres stored to ordinary materialization retains values and permits later writes -01:13 +200 -2: test/computed_test.dart: computed postgres explicit renamed expressions preserve indexes, checks, references and views -01:13 +201 -2: test/computed_test.dart: computed postgres invalid recomputation rolls back the table and migration history -01:13 +202 -2: test/computed_test.dart: computed postgres decimal computed SQL keeps precision coercion and imports exactly once -01:14 +203 -2: test/computed_test.dart: mode conversions need explicit plans and historical copy/backfill cannot write computed columns -01:14 +204 -2: loading test/named_sql_test.dart -01:14 +204 -2: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -01:14 +205 -2: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -01:14 +206 -2: test/named_sql_test.dart: generated client is deterministic and checked against its Dart declaration -01:16 +207 -2: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -01:16 +208 -2: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -01:16 +209 -2: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -01:16 +210 -2: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -01:16 +211 -2: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -01:16 +212 -2: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -01:17 +213 -2: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -01:17 +214 -2: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -01:17 +215 -2: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -01:17 +216 -2: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -01:17 +217 -2: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -01:17 +218 -2: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -01:17 +219 -2: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -01:17 +220 -2: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -01:17 +221 -2: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -01:17 +222 -2: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -01:17 +223 -2: loading test/named_sql_generation_test.dart -01:18 +223 -2: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -01:21 +224 -2: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -01:23 +225 -2: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -01:25 +226 -2: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -01:25 +227 -2: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -01:44 +228 -2: loading test/decimal_average_test.dart -01:44 +228 -2: test/decimal_average_test.dart: integer fractions round once and only bound their final result -01:45 +229 -2: test/decimal_average_test.dart: decimal average sqlite empty null and constant inputs retain aggregate semantics -01:45 +230 -2: test/decimal_average_test.dart: decimal average sqlite all rounding modes match exact signed sums across scales -01:45 +231 -2: test/decimal_average_test.dart: decimal average sqlite valid means survive sums exceeding the finite numeric range -01:47 +232 -2: test/decimal_average_test.dart: decimal average sqlite minimum fractions and outermost rounding scales stay exact -01:47 +233 -2: test/decimal_average_test.dart: decimal average sqlite grouping HAVING ordering and CTE references preserve the mean -01:47 +234 -2: test/decimal_average_test.dart: decimal average sqlite illegal window nesting and aggregate assignments fail before SQL -01:47 +235 -2: test/decimal_average_test.dart: decimal average sqlite windows and rounded window means respect partitions and pagination -01:47 +236 -2: test/decimal_average_test.dart: decimal average sqlite correlated means can update values and failures roll back transactions -01:47 +237 -2: test/decimal_average_test.dart: decimal average sqlite batched relationship windows keep per-parent limits -01:47 +238 -2: test/decimal_average_test.dart: decimal average sqlite sliding windows remove values, reset empty frames and reject varying policies -01:47 +239 -2: test/decimal_average_test.dart: decimal average sqlite window means of grouped sums run after HAVING -01:47 +240 -2: test/decimal_average_test.dart: decimal average postgres empty null and constant inputs retain aggregate semantics -01:47 +241 -2: test/decimal_average_test.dart: decimal average postgres all rounding modes match exact signed sums across scales -01:47 +242 -2: test/decimal_average_test.dart: decimal average postgres valid means survive sums exceeding the finite numeric range -01:47 +243 -2: test/decimal_average_test.dart: decimal average postgres minimum fractions and outermost rounding scales stay exact -01:48 +244 -2: test/decimal_average_test.dart: decimal average postgres grouping HAVING ordering and CTE references preserve the mean -01:48 +245 -2: test/decimal_average_test.dart: decimal average postgres illegal window nesting and aggregate assignments fail before SQL -01:48 +246 -2: test/decimal_average_test.dart: decimal average postgres windows and rounded window means respect partitions and pagination -01:48 +247 -2: test/decimal_average_test.dart: decimal average postgres correlated means can update values and failures roll back transactions -01:48 +248 -2: test/decimal_average_test.dart: decimal average postgres batched relationship windows keep per-parent limits -01:48 +249 -2: test/decimal_average_test.dart: decimal average postgres window means of grouped sums run after HAVING -01:48 +250 -2: test/decimal_average_test.dart: decimal average postgres volatile inputs and window ordering are evaluated once per row -01:48 +251 -2: loading test/instant_test.dart -01:48 +251 -2: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -01:48 +252 -2: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -01:49 +253 -2: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -01:49 +254 -2: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -01:49 +255 -2: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -01:49 +256 -2: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -01:49 +257 -2: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -01:51 +258 -2: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -01:51 +259 -2: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -01:51 +260 -2: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -01:51 +261 -2: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -01:51 +262 -2: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -01:51 +263 -2: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -01:51 +264 -2: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -01:51 +265 -2: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -01:51 +266 -2: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -01:52 +267 -2: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -01:52 +268 -2: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -01:52 +269 -2: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -01:52 +270 -2: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -01:52 +271 -2: loading test/codec_test.dart -01:52 +271 -2: test/codec_test.dart: numeric aggregate text decodes without integer rounding or overflow -01:52 +272 -2: test/codec_test.dart: erased codecs keep checked argument types without function getter casts -01:52 +273 -2: test/codec_test.dart: mapped domain codecs preserve one decode and one encode boundary -01:52 +274 -2: test/codec_test.dart: parsed JSON scalars and document presence have unambiguous decoding -01:52 +275 -2: test/codec_test.dart: enum mappings reject duplicates and do not follow mutable caller maps -01:52 +276 -2: loading test/sql_real_test.dart -01:52 +276 -2: test/sql_real_test.dart: sqlite explicitly typed REAL parameters preserve floating storage -01:52 +277 -2: test/sql_real_test.dart: postgres explicitly typed REAL parameters preserve floating storage -01:52 +278 -2: loading test/migration_source_test.dart -01:52 +278 -2: test/migration_source_test.dart: Dart emission preserves storage metadata and every operation without application imports -02:00 +279 -2: test/migration_source_test.dart: project CLI creates Dart history, upgrades a database and preserves recorded fingerprints -03:13 +280 -2: test/migration_source_test.dart: AOT migration bundle works without Dart sources or migration assets -03:24 +281 -2: loading test/generator_test.dart -03:24 +281 -2: test/generator_test.dart: (setUpAll) -03:24 +281 -2: test/generator_test.dart: client and snapshot paths cannot overwrite the declaration -03:24 +282 -2: test/generator_test.dart: record annotations, physical names, relations and snapshots -03:26 +283 -2: test/generator_test.dart: output is deterministic and matches the committed generated client -03:31 +284 -2: test/generator_test.dart: composite keys and more than six fields generate analyzable code -03:32 +285 -2: test/generator_test.dart: computed keys are rejected instead of executing a fake row -03:32 +286 -2: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -03:34 +287 -2: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -03:37 +288 -2: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty overrides and duplicate names -03:38 +289 -2: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -03:38 +290 -2: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -03:39 +291 -2: test/generator_test.dart: invalid custom declaration: wrong_codec -03:39 +292 -2: test/generator_test.dart: invalid custom declaration: nullable_codec -03:40 +293 -2: test/generator_test.dart: invalid custom declaration: erased_extension -03:40 +294 -2: test/generator_test.dart: invalid custom declaration: unknown_storage -03:40 +295 -2: test/generator_test.dart: invalid custom declaration: private_codec -03:40 +296 -2: test/generator_test.dart: invalid custom declaration: private_type -03:41 +297 -2: test/generator_test.dart: invalid custom declaration: inline_codec -03:41 +298 -2: test/generator_test.dart: invalid custom declaration: duplicate_codec -03:41 +299 -2: test/generator_test.dart: invalid custom declaration: missing_codec -03:41 +300 -2: test/generator_test.dart: invalid custom declaration: duplicate_labels -03:41 +301 -2: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -03:42 +302 -2: test/generator_test.dart: foreign keys require unique targets -03:42 +303 -2: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -03:42 +304 -2: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -03:43 +305 -2: test/generator_test.dart: invalid query-only relation: lookup_computed -03:43 +306 -2: test/generator_test.dart: invalid query-only relation: lookup_codec -03:43 +307 -2: test/generator_test.dart: invalid query-only relation: lookup_inverse -03:43 +308 -2: test/generator_test.dart: invalid query-only relation: lookup_type -03:44 +309 -2: test/generator_test.dart: invalid query-only relation: lookup_delete -03:44 +310 -2: test/generator_test.dart: Dart type errors stop generation -03:44 +311 -2: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -03:45 +312 -2: test/generator_test.dart: (tearDownAll) -03:45 +312 -2: loading test/transaction_test.dart -03:45 +312 -2: test/transaction_test.dart: transaction control sqlite idle callback expires, rolls back and cannot issue late SQL -03:45 +313 -2: test/transaction_test.dart: transaction control sqlite deadline interrupts active SQL before releasing and reusing the lease -03:45 +314 -2: test/transaction_test.dart: transaction control sqlite interrupted write rolls back and leaves the connection usable -03:45 +315 -2: test/transaction_test.dart: transaction control sqlite explicit transaction cancellation stops an idle callback and active SQL -03:45 +316 -2: test/transaction_test.dart: transaction control sqlite global transaction cancellation also abandons acquisition -03:45 +317 -2: test/transaction_test.dart: transaction control sqlite savepoint inherits deadline, drains and rolls back through raw cleanup -03:45 +318 -2: test/transaction_test.dart: transaction control sqlite deadline closes a paused transactional cursor before rollback -03:45 +319 -2: test/transaction_test.dart: transaction control sqlite deadline interrupts an active transactional cursor fetch -03:45 +320 -2: test/transaction_test.dart: transaction control sqlite transaction clock starts after acquisition and a borrowed session recovers -03:46 +321 -2: test/transaction_test.dart: transaction control sqlite failed rollback is explicit and discards an uncertain connection -03:46 +322 -2: test/transaction_test.dart: transaction control sqlite transaction controls reject adapters without actual cancellation -03:46 +323 -2: test/transaction_test.dart: transaction control sqlite shorter statement limit retains its error and rolls back the transaction -03:46 +324 -2: test/transaction_test.dart: transaction control sqlite monotonic deadline prevents commit after a CPU-bound callback -03:46 +325 -2: test/transaction_test.dart: transaction control sqlite completed scopes remove their timers and cancellation listeners -03:46 +326 -2: test/transaction_test.dart: transaction control sqlite confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:46 +327 -2: test/transaction_test.dart: transaction control sqlite deferred constraint rejection at COMMIT is a known failure -03:46 +328 -2: test/transaction_test.dart: transaction control sqlite invalid deadlines and already cancelled tokens never begin a transaction -03:46 +329 -2: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=false -03:46 +330 -2: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=true -03:46 +331 -2: test/transaction_test.dart: transaction control postgres idle callback expires, rolls back and cannot issue late SQL -03:46 +332 -2: test/transaction_test.dart: transaction control postgres deadline interrupts active SQL before releasing and reusing the lease -03:46 +333 -2: test/transaction_test.dart: transaction control postgres interrupted write rolls back and leaves the connection usable -03:46 +334 -2: test/transaction_test.dart: transaction control postgres explicit transaction cancellation stops an idle callback and active SQL -03:46 +335 -2: test/transaction_test.dart: transaction control postgres global transaction cancellation also abandons acquisition -03:46 +336 -2: test/transaction_test.dart: transaction control postgres savepoint inherits deadline, drains and rolls back through raw cleanup -03:47 +337 -2: test/transaction_test.dart: transaction control postgres deadline closes a paused transactional cursor before rollback -03:47 +338 -2: test/transaction_test.dart: transaction control postgres deadline interrupts an active transactional cursor fetch -03:47 +339 -2: test/transaction_test.dart: transaction control postgres transaction clock starts after acquisition and a borrowed session recovers -03:47 +340 -2: test/transaction_test.dart: transaction control postgres failed rollback is explicit and discards an uncertain connection -03:47 +341 -2: test/transaction_test.dart: transaction control postgres transaction controls reject adapters without actual cancellation -03:47 +342 -2: test/transaction_test.dart: transaction control postgres shorter statement limit retains its error and rolls back the transaction -03:47 +343 -2: test/transaction_test.dart: transaction control postgres monotonic deadline prevents commit after a CPU-bound callback -03:47 +344 -2: test/transaction_test.dart: transaction control postgres completed scopes remove their timers and cancellation listeners -03:48 +345 -2: test/transaction_test.dart: transaction control postgres confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:48 +346 -2: test/transaction_test.dart: transaction control postgres deferred constraint rejection at COMMIT is a known failure -03:48 +347 -2: test/transaction_test.dart: transaction control postgres invalid deadlines and already cancelled tokens never begin a transaction -03:48 +348 -2: test/transaction_test.dart: transaction control postgres SQLSTATE 40003 at COMMIT is never classified as a confirmed rejection -03:48 +349 -2: test/transaction_test.dart: transaction control postgres real PostgreSQL serialization failure is classified after rollback -03:48 +350 -2: test/transaction_test.dart: transaction control postgres real PostgreSQL deadlock rolls back exactly one transaction -03:49 +351 -2: test/transaction_test.dart: SQLite COMMIT busy is a known rejection followed by verified rollback -03:49 +352 -2: test/transaction_test.dart: SQLite deadline while BEGIN waits for a writer preserves its connection -03:49 +353 -2: loading test/backfill_keys_test.dart -03:49 +353 -2: test/backfill_keys_test.dart: backfill keys sqlite integer cursors preserve native values across resume -03:49 +354 -2: test/backfill_keys_test.dart: backfill keys sqlite bigint cursors preserve native values across resume -03:49 +355 -2: test/backfill_keys_test.dart: backfill keys sqlite text cursors preserve native values across resume -03:49 +356 -2: test/backfill_keys_test.dart: backfill keys sqlite real cursors preserve native values across resume -03:49 +357 -2: test/backfill_keys_test.dart: backfill keys sqlite boolean cursors preserve native values across resume -03:49 +358 -2: test/backfill_keys_test.dart: backfill keys sqlite timestamp cursors preserve native values across resume -03:49 +359 -2: test/backfill_keys_test.dart: backfill keys sqlite blob cursors preserve native values across resume -03:49 +360 -2: test/backfill_keys_test.dart: backfill keys sqlite composite key batches account for every bound parameter -03:49 +361 -2: test/backfill_keys_test.dart: backfill keys postgres integer cursors preserve native values across resume -03:49 +362 -2: test/backfill_keys_test.dart: backfill keys postgres bigint cursors preserve native values across resume -03:49 +363 -2: test/backfill_keys_test.dart: backfill keys postgres text cursors preserve native values across resume -03:50 +364 -2: test/backfill_keys_test.dart: backfill keys postgres real cursors preserve native values across resume -03:50 +365 -2: test/backfill_keys_test.dart: backfill keys postgres boolean cursors preserve native values across resume -03:50 +366 -2: test/backfill_keys_test.dart: backfill keys postgres timestamp cursors preserve native values across resume -03:50 +367 -2: test/backfill_keys_test.dart: backfill keys postgres blob cursors preserve native values across resume -03:50 +368 -2: test/backfill_keys_test.dart: backfill keys postgres composite key batches account for every bound parameter -03:50 +369 -2: test/backfill_keys_test.dart: invalid key, assignments and completion declarations fail before execution -03:50 +370 -2: loading test/retry_test.dart -03:50 +370 -2: test/retry_test.dart: retry sqlite opt-in retries roll back all writes and use new closed-over views -03:50 +371 -2: test/retry_test.dart: retry sqlite default is one attempt; an explicit attempt limit is exact -03:50 +372 -2: test/retry_test.dart: retry sqlite application and constraint errors and caught statement failures do not replay -03:50 +373 -2: test/retry_test.dart: retry sqlite unknown commit and unconfirmed rollback prohibit replay -03:50 +374 -2: test/retry_test.dart: retry sqlite retry time budget bounds active SQL and does not retry cancellation -03:50 +375 -2: test/retry_test.dart: retry sqlite deadline and cancellation interrupt backoff without another callback -03:50 +376 -2: test/retry_test.dart: retry sqlite time budget includes acquisition and abandons late callbacks -03:50 +377 -2: test/retry_test.dart: retry sqlite transaction timeout is shared across attempts and sessions recover after expiry -03:50 +378 -2: test/retry_test.dart: retry sqlite subscriptions publish only the final committed retry result -03:50 +379 -2: test/retry_test.dart: retry sqlite invalid policies reject before SQL -03:50 +380 -2: test/retry_test.dart: retry postgres opt-in retries roll back all writes and use new closed-over views -03:50 +381 -2: test/retry_test.dart: retry postgres default is one attempt; an explicit attempt limit is exact -03:50 +382 -2: test/retry_test.dart: retry postgres application and constraint errors and caught statement failures do not replay -03:50 +383 -2: test/retry_test.dart: retry postgres unknown commit and unconfirmed rollback prohibit replay -03:50 +384 -2: test/retry_test.dart: retry postgres retry time budget bounds active SQL and does not retry cancellation -03:51 +385 -2: test/retry_test.dart: retry postgres deadline and cancellation interrupt backoff without another callback -03:51 +386 -2: test/retry_test.dart: retry postgres time budget includes acquisition and abandons late callbacks -03:51 +387 -2: test/retry_test.dart: retry postgres transaction timeout is shared across attempts and sessions recover after expiry -03:51 +388 -2: test/retry_test.dart: retry postgres subscriptions publish only the final committed retry result -03:51 +389 -2: test/retry_test.dart: retry postgres invalid policies reject before SQL -03:51 +390 -2: test/retry_test.dart: retry postgres real unknown COMMIT response prohibits opted-in retry -03:51 +391 -2: test/retry_test.dart: retry postgres real serialization conflict repeats the reads and decisions -03:51 +392 -2: test/retry_test.dart: retry postgres real deadlock retries only the aborted callback -03:52 +393 -2: test/retry_test.dart: SQLite WAL snapshot conflict replays reads after rollback -03:52 +394 -2: test/retry_test.dart: SQLite real busy COMMIT: success -03:52 +395 -2: test/retry_test.dart: SQLite real busy COMMIT: exhausted -03:52 +396 -2: test/retry_test.dart: SQLite real busy COMMIT: deadline -03:52 +397 -2: test/retry_test.dart: SQLite real busy COMMIT: cancel -03:52 +398 -2: test/retry_test.dart: SQLite real busy COMMIT: shared-budget -03:53 +399 -2: loading test/builder_test.dart -03:53 +399 -2: test/builder_test.dart: build assets and standalone CLI produce identical client and snapshot -03:56 +400 -2: test/builder_test.dart: multiple explicit schema roots resolve shared public types under lib -03:56 +401 -2: test/builder_test.dart: invalid semantic_error never publishes partial outputs -03:57 +402 -2: test/builder_test.dart: invalid enum_labels never publishes partial outputs -03:58 +403 -2: test/builder_test.dart: invalid no_models never publishes partial outputs -03:58 +404 -2: test/builder_test.dart: invalid part_file never publishes partial outputs -03:59 +405 -2: test/builder_test.dart: unknown options fail instead of silently changing build behavior -03:59 +406 -2: test/builder_test.dart: schema resolution can consume an earlier builder output -04:00 +407 -2: loading test/temporal_precision_test.dart -04:00 +407 -2: test/temporal_precision_test.dart: temporal precision sqlite generated writes, defaults and computed columns use database precision -04:00 +408 -2: test/temporal_precision_test.dart: temporal precision sqlite SQL casts match all precisions on both sides of the PostgreSQL epoch -04:00 +409 -2: test/temporal_precision_test.dart: temporal precision sqlite nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:00 +410 -2: test/temporal_precision_test.dart: temporal precision sqlite rounded keys work in batch writes, upserts and foreign-key relation loading -04:00 +411 -2: test/temporal_precision_test.dart: temporal precision sqlite catalog import preserves precision, defaults and computed expressions -04:02 +412 -2: test/temporal_precision_test.dart: temporal precision sqlite narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:02 +413 -2: test/temporal_precision_test.dart: temporal precision sqlite timestamp defaults and explicit six-digit catalogs retain precision -04:02 +414 -2: test/temporal_precision_test.dart: temporal precision sqlite extended timestamps round in SQL without total-microsecond overflow -04:02 +415 -2: test/temporal_precision_test.dart: temporal precision sqlite raw SQL respects native or managed precision constraints -04:02 +416 -2: test/temporal_precision_test.dart: temporal precision postgres generated writes, defaults and computed columns use database precision -04:02 +417 -2: test/temporal_precision_test.dart: temporal precision postgres SQL casts match all precisions on both sides of the PostgreSQL epoch -04:02 +418 -2: test/temporal_precision_test.dart: temporal precision postgres nulls, CTE references, grouping, streams and wrong scopes retain type semantics -04:02 +419 -2: test/temporal_precision_test.dart: temporal precision postgres rounded keys work in batch writes, upserts and foreign-key relation loading -04:02 +420 -2: test/temporal_precision_test.dart: temporal precision postgres catalog import preserves precision, defaults and computed expressions -04:03 +421 -2: test/temporal_precision_test.dart: temporal precision postgres narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -04:03 +422 -2: test/temporal_precision_test.dart: temporal precision postgres timestamp defaults and explicit six-digit catalogs retain precision -04:03 +423 -2: test/temporal_precision_test.dart: temporal precision postgres extended timestamps round in SQL without total-microsecond overflow -04:03 +424 -2: test/temporal_precision_test.dart: temporal precision postgres raw SQL respects native or managed precision constraints -04:03 +425 -2: test/temporal_precision_test.dart: value rounding keeps extended ranges and rejects overflow/invalid precision -04:03 +426 -2: test/temporal_precision_test.dart: default and explicit six preserve historical snapshots and migration checksums -04:03 +427 -2: test/temporal_precision_test.dart: generator rejects invalid annotations and preserves generated fixture metadata -04:05 +428 -2: loading test/observation_test.dart -04:05 +428 -2: test/observation_test.dart: observation sqlite actual lease queue and synchronous mapping have separate clocks -04:05 +429 -2: test/observation_test.dart: observation sqlite expired acquisition emits one failure and no late SQL -04:05 +430 -2: test/observation_test.dart: observation sqlite cancelled acquisition emits one failure and no late SQL -04:05 +431 -2: test/observation_test.dart: observation sqlite sessions transactions and savepoints inherit hooks and report reused leases -04:05 +432 -2: test/observation_test.dart: observation sqlite nested reads attribute decoding to each statement without repeating SQL -04:05 +433 -2: test/observation_test.dart: observation sqlite SQL and mapper failures belong to their own phases -04:05 +434 -2: test/observation_test.dart: observation sqlite cursor batches and returning writes produce decoding observations -04:05 +435 -2: test/observation_test.dart: observation sqlite observer exceptions preserve successful commits and original mapper errors -04:05 +436 -2: test/observation_test.dart: observation postgres actual lease queue and synchronous mapping have separate clocks -04:06 +437 -2: test/observation_test.dart: observation postgres expired acquisition emits one failure and no late SQL -04:06 +438 -2: test/observation_test.dart: observation postgres cancelled acquisition emits one failure and no late SQL -04:06 +439 -2: test/observation_test.dart: observation postgres sessions transactions and savepoints inherit hooks and report reused leases -04:06 +440 -2: test/observation_test.dart: observation postgres nested reads attribute decoding to each statement without repeating SQL -04:06 +441 -2: test/observation_test.dart: observation postgres SQL and mapper failures belong to their own phases -04:06 +442 -2: test/observation_test.dart: observation postgres cursor batches and returning writes produce decoding observations -04:06 +443 -2: test/observation_test.dart: observation postgres observer exceptions preserve successful commits and original mapper errors -04:06 +444 -2: test/observation_test.dart: synchronous driver acquisition failures are reported once -04:06 +445 -2: loading test/integer_test.dart -04:06 +445 -2: test/integer_test.dart: integer widths sqlite storage bounds, nulls and wider sums keep the normal integer value codec -04:06 +446 -2: test/integer_test.dart: integer widths sqlite relations, union and cursors share int expressions across physical widths -04:06 +447 -2: test/integer_test.dart: integer widths sqlite defaulted and quoted columns retain enforced nullable bounds -04:06 +448 -2: test/integer_test.dart: integer widths sqlite Dart snapshots and imports retain widths without confusing value codecs -04:08 +449 -2: test/integer_test.dart: integer widths sqlite widening and narrowing use reviewed conversions with atomic failure recovery -04:08 +450 -2: test/integer_test.dart: integer widths sqlite column renames preserve width metadata and range constraints -04:08 +451 -2: test/integer_test.dart: integer widths sqlite historical bounded backfills verify widths and resume normally -04:08 +452 -2: test/integer_test.dart: integer widths sqlite changing an identity width preserves generation and rejects out-of-range explicit IDs -04:08 +453 -2: test/integer_test.dart: integer widths sqlite quoted defaults and comments cannot masquerade as managed range checks -04:08 +454 -2: test/integer_test.dart: integer widths sqlite extra weaker expressions remain unmanaged and cannot claim a narrower width -04:08 +455 -2: test/integer_test.dart: integer widths postgres storage bounds, nulls and wider sums keep the normal integer value codec -04:08 +456 -2: test/integer_test.dart: integer widths postgres relations, union and cursors share int expressions across physical widths -04:08 +457 -2: test/integer_test.dart: integer widths postgres defaulted and quoted columns retain enforced nullable bounds -04:08 +458 -2: test/integer_test.dart: integer widths postgres Dart snapshots and imports retain widths without confusing value codecs -04:09 +459 -2: test/integer_test.dart: integer widths postgres widening and narrowing use reviewed conversions with atomic failure recovery -04:09 +460 -2: test/integer_test.dart: integer widths postgres column renames preserve width metadata and range constraints -04:09 +461 -2: test/integer_test.dart: integer widths postgres historical bounded backfills verify widths and resume normally -04:09 +462 -2: test/integer_test.dart: integer widths postgres changing an identity width preserves generation and rejects out-of-range explicit IDs -04:09 +463 -2: test/integer_test.dart: analyzer validates width annotations and integer-backed domain codecs -04:10 +464 -2: test/integer_test.dart: width declarations reject wrong kinds and ranges without changing default snapshots -04:10 +465 -2: loading test/relation_test.dart -04:10 +465 -2: test/relation_test.dart: sqlite automatic unique joins preserve root pagination in one statement -04:10 +466 -2: test/relation_test.dart: sqlite all-null projections still distinguish a present row from absence -04:10 +467 -2: test/relation_test.dart: sqlite explicit batch deduplicates composite keys and skips partial nulls -04:10 +468 -2: test/relation_test.dart: sqlite composite chunks include filter and window parameters with nested joins -04:10 +469 -2: test/relation_test.dart: sqlite nested self joins and two edges to the same table use distinct aliases -04:10 +470 -2: test/relation_test.dart: sqlite batch collection below a joined optional parent loads on the same plan -04:10 +471 -2: test/relation_test.dart: sqlite join refuses unproven cardinality while automatic batch checks it -04:10 +472 -2: test/relation_test.dart: sqlite filters and to-one pagination do not drop root rows -04:10 +473 -2: test/relation_test.dart: sqlite joined records survive CTE projection and cursor decoding -04:10 +474 -2: test/relation_test.dart: sqlite required tests row presence independently of nullable selected values -04:10 +475 -2: test/relation_test.dart: sqlite relation aggregates restore the alias used by a joined selection -04:10 +476 -2: test/relation_test.dart: sqlite reusing an identical edge deduplicates its join -04:10 +477 -2: test/relation_test.dart: sqlite different filters need fresh relation occurrences -04:10 +478 -2: test/relation_test.dart: sqlite large composite batches avoid expression-depth failure -04:10 +479 -2: test/relation_test.dart: postgres automatic unique joins preserve root pagination in one statement -04:11 +480 -2: test/relation_test.dart: postgres all-null projections still distinguish a present row from absence -04:11 +481 -2: test/relation_test.dart: postgres explicit batch deduplicates composite keys and skips partial nulls -04:11 +482 -2: test/relation_test.dart: postgres composite chunks include filter and window parameters with nested joins -04:11 +483 -2: test/relation_test.dart: postgres nested self joins and two edges to the same table use distinct aliases -04:11 +484 -2: test/relation_test.dart: postgres batch collection below a joined optional parent loads on the same plan -04:11 +485 -2: test/relation_test.dart: postgres join refuses unproven cardinality while automatic batch checks it -04:11 +486 -2: test/relation_test.dart: postgres filters and to-one pagination do not drop root rows -04:11 +487 -2: test/relation_test.dart: postgres joined records survive CTE projection and cursor decoding -04:11 +488 -2: test/relation_test.dart: postgres required tests row presence independently of nullable selected values -04:11 +489 -2: test/relation_test.dart: postgres relation aggregates restore the alias used by a joined selection -04:11 +490 -2: test/relation_test.dart: postgres reusing an identical edge deduplicates its join -04:11 +491 -2: test/relation_test.dart: postgres different filters need fresh relation occurrences -04:11 +492 -2: test/relation_test.dart: postgres large composite batches avoid expression-depth failure -04:11 +493 -2: loading test/generated_database_test.dart -04:11 +493 -2: test/generated_database_test.dart: (setUpAll) -04:11 +493 -2: test/generated_database_test.dart: generated sqlite generated client, DDL, relations, dates and patches work together -04:11 +494 -2: test/generated_database_test.dart: generated sqlite planning is read-only and repeat migration is a no-op -04:11 +495 -2: test/generated_database_test.dart: generated sqlite compiled physical snapshot and full managed catalog comparison -04:11 +496 -2: test/generated_database_test.dart: generated sqlite baseline preserves existing rows and verifies keys before recording history -04:11 +497 -2: test/generated_database_test.dart: generated sqlite full verification sees defaults and additional unique constraints -04:11 +498 -2: test/generated_database_test.dart: generated sqlite history tampering and missing applied migrations fail -04:11 +499 -2: test/generated_database_test.dart: generated sqlite failed DDL rolls back schema and history atomically -04:11 +500 -2: test/generated_database_test.dart: generated sqlite actual column drift is detected independently of history -04:11 +501 -2: test/generated_database_test.dart: generated sqlite transaction control cannot be hidden behind SQL comments -04:11 +502 -2: test/generated_database_test.dart: generated sqlite concurrent migration callers serialize against shared history -04:11 +503 -2: test/generated_database_test.dart: generated postgres generated client, DDL, relations, dates and patches work together -04:11 +504 -2: test/generated_database_test.dart: generated postgres planning is read-only and repeat migration is a no-op -04:11 +505 -2: test/generated_database_test.dart: generated postgres compiled physical snapshot and full managed catalog comparison -04:11 +506 -2: test/generated_database_test.dart: generated postgres baseline preserves existing rows and verifies keys before recording history -04:11 +507 -2: test/generated_database_test.dart: generated postgres full verification sees defaults and additional unique constraints -04:11 +508 -2: test/generated_database_test.dart: generated postgres history tampering and missing applied migrations fail -04:11 +509 -2: test/generated_database_test.dart: generated postgres failed DDL rolls back schema and history atomically -04:11 +510 -2: test/generated_database_test.dart: generated postgres actual column drift is detected independently of history -04:11 +511 -2: test/generated_database_test.dart: generated postgres transaction control cannot be hidden behind SQL comments -04:11 +512 -2: test/generated_database_test.dart: generated postgres concurrent migration callers serialize against shared history -04:11 +513 -2: test/generated_database_test.dart: (tearDownAll) -04:11 +513 -2: loading test/import_test.dart -04:12 +513 -2: test/import_test.dart: catalog import sqlite imports records, identities, defaults and constraints then baselines unchanged rows -04:15 +514 -2: test/import_test.dart: catalog import sqlite normalizes reserved, colliding and hostile identifiers without changing physical names -04:16 +515 -2: test/import_test.dart: catalog import sqlite composite and self relations preserve key order and unique indexes -04:17 +516 -2: test/import_test.dart: catalog import sqlite unsupported storage excludes its table while computed SQL imports with a review note -04:17 +517 -2: test/import_test.dart: catalog import sqlite read-only snapshot and selected scope reject temporary shadowing and active transactions -04:17 +518 -2: test/import_test.dart: catalog import sqlite physical storage mapping does not infer domain types from data -04:18 +519 -2: test/import_test.dart: catalog import sqlite views triggers and specialized indexes remain in the review report -04:18 +520 -2: test/import_test.dart: catalog import sqlite nullable primary keys, virtual tables and their storage tables need explicit handling -04:18 +521 -2: test/import_test.dart: catalog import sqlite foreign keys with coercible but different Dart types are reported -04:18 +522 -2: test/import_test.dart: catalog import postgres imports records, identities, defaults and constraints then baselines unchanged rows -04:21 +523 -2: test/import_test.dart: catalog import postgres normalizes reserved, colliding and hostile identifiers without changing physical names -04:22 +524 -2: test/import_test.dart: catalog import postgres composite and self relations preserve key order and unique indexes -04:22 +525 -2: test/import_test.dart: catalog import postgres unsupported storage excludes its table while computed SQL imports with a review note -04:23 +526 -2: test/import_test.dart: catalog import postgres read-only snapshot and selected scope reject temporary shadowing and active transactions -04:23 +527 -2: test/import_test.dart: catalog import postgres physical storage mapping does not infer domain types from data -04:23 +528 -2: test/import_test.dart: catalog import postgres views triggers and specialized indexes remain in the review report -04:23 +529 -2: test/import_test.dart: catalog import postgres policy reports retain roles, commands, mode and row-security state -04:24 +530 -2: test/import_test.dart: catalog import postgres always identity and inherited tables are not flattened into ordinary writable models -04:24 +531 -2: loading test/database_test.dart -04:24 +531 -2: test/database_test.dart: sqlite typed CRUD, exact projection and parameter binding -04:24 +532 -2: test/database_test.dart: sqlite record and DTO mapping run only when decoding -04:24 +533 -2: test/database_test.dart: sqlite immutable filters, empty IN, null and pagination -04:24 +534 -2: test/database_test.dart: sqlite cross-table identity and duplicate assignments rejected before IO -04:24 +535 -2: test/database_test.dart: sqlite commit, rollback, savepoint recovery and escaped session -04:24 +536 -2: test/database_test.dart: sqlite caught statement failures cannot masquerade as a commit -04:24 +537 -2: test/database_test.dart: sqlite recoverable constraint errors stay within their savepoint -04:24 +538 -2: test/database_test.dart: sqlite unawaited work is drained and rolled back -04:24 +539 -2: test/database_test.dart: sqlite close waits for active operations and rejects further use -04:24 +540 -2: test/database_test.dart: sqlite leased sessions retain one connection across transactions and expire -04:24 +541 -2: test/database_test.dart: sqlite native upsert updates atomically using existing and incoming values -04:24 +542 -2: test/database_test.dart: sqlite bulk inserts split at parameter limits and opt into returning -04:24 +543 -2: test/database_test.dart: sqlite a failed later bulk chunk rolls back all previous chunks -04:24 +544 -2: test/database_test.dart: sqlite bulk optional field shapes preserve defaults and explicit null -04:24 +545 -2: test/database_test.dart: sqlite relations CTEs preserve SQL column identity through mapped projections -04:24 +546 -2: test/database_test.dart: sqlite relations explicit aliases support multiple joins and self joins -04:24 +547 -2: test/database_test.dart: sqlite relations left join distinguishes a missing row from an all-null selection -04:24 +548 -2: test/database_test.dart: sqlite relations correlated scalar, IN and EXISTS use nested SQL scopes -04:24 +549 -2: test/database_test.dart: sqlite relations grouping is checked and window functions preserve row cardinality -04:24 +550 -2: test/database_test.dart: sqlite relations root pagination and each parent limit use two queries -04:24 +551 -2: test/database_test.dart: sqlite relations empty roots skip child queries; empty lists remain typed lists -04:24 +552 -2: test/database_test.dart: sqlite relations nested relation projections and optional all-null projection -04:24 +553 -2: test/database_test.dart: sqlite relations correlated any, none, count and every with SQL nulls -04:24 +554 -2: test/database_test.dart: sqlite relations relation parameters bind correctly in projection and filter -04:24 +555 -2: test/database_test.dart: sqlite keyset pagination uses a unique tie breaker in mixed directions -04:24 +556 -2: test/database_test.dart: sqlite keyset pagination handles null order and validates versioned tokens -04:24 +557 -2: test/database_test.dart: postgres typed CRUD, exact projection and parameter binding -04:24 +558 -2: test/database_test.dart: postgres record and DTO mapping run only when decoding -04:24 +559 -2: test/database_test.dart: postgres immutable filters, empty IN, null and pagination -04:24 +560 -2: test/database_test.dart: postgres cross-table identity and duplicate assignments rejected before IO -04:24 +561 -2: test/database_test.dart: postgres commit, rollback, savepoint recovery and escaped session -04:24 +562 -2: test/database_test.dart: postgres caught statement failures cannot masquerade as a commit -04:24 +563 -2: test/database_test.dart: postgres recoverable constraint errors stay within their savepoint -04:24 +564 -2: test/database_test.dart: postgres unawaited work is drained and rolled back -04:24 +565 -2: test/database_test.dart: postgres close waits for active operations and rejects further use -04:24 +566 -2: test/database_test.dart: postgres leased sessions retain one connection across transactions and expire -04:24 +567 -2: test/database_test.dart: postgres native upsert updates atomically using existing and incoming values -04:24 +568 -2: test/database_test.dart: postgres bulk inserts split at parameter limits and opt into returning -04:24 +569 -2: test/database_test.dart: postgres a failed later bulk chunk rolls back all previous chunks -04:24 +570 -2: test/database_test.dart: postgres bulk optional field shapes preserve defaults and explicit null -04:24 +571 -2: test/database_test.dart: postgres relations CTEs preserve SQL column identity through mapped projections -04:24 +572 -2: test/database_test.dart: postgres relations explicit aliases support multiple joins and self joins -04:24 +573 -2: test/database_test.dart: postgres relations left join distinguishes a missing row from an all-null selection -04:24 +574 -2: test/database_test.dart: postgres relations correlated scalar, IN and EXISTS use nested SQL scopes -04:24 +575 -2: test/database_test.dart: postgres relations grouping is checked and window functions preserve row cardinality -04:24 +576 -2: test/database_test.dart: postgres relations root pagination and each parent limit use two queries -04:24 +577 -2: test/database_test.dart: postgres relations empty roots skip child queries; empty lists remain typed lists -04:24 +578 -2: test/database_test.dart: postgres relations nested relation projections and optional all-null projection -04:24 +579 -2: test/database_test.dart: postgres relations correlated any, none, count and every with SQL nulls -04:24 +580 -2: test/database_test.dart: postgres relations relation parameters bind correctly in projection and filter -04:24 +581 -2: test/database_test.dart: postgres keyset pagination uses a unique tie breaker in mixed directions -04:24 +582 -2: test/database_test.dart: postgres keyset pagination handles null order and validates versioned tokens -04:24 +583 -2: loading test/union_test.dart -04:24 +583 -2: test/union_test.dart: union sqlite SQL duplicate and NULL semantics use one statement -04:24 +584 -2: test/union_test.dart: union sqlite heterogeneous records map only after SQL union and final pagination -04:24 +585 -2: test/union_test.dart: union sqlite operand limits, order, parameters and final filter stay scoped -04:24 +586 -2: test/union_test.dart: union sqlite repeated SQL expressions preserve positional columns -04:24 +587 -2: test/union_test.dart: union sqlite nested sets preserve association, CTE exports and streaming -04:24 +588 -2: test/union_test.dart: union sqlite same CTE names in distinct operands stay in separate scopes -04:24 +589 -2: test/union_test.dart: union sqlite SQL scalar, IN subqueries, aggregate and zero-row results compose -04:24 +590 -2: test/union_test.dart: union sqlite grouped branches and window branches remain SQL queries -04:24 +591 -2: test/union_test.dart: union sqlite bound constant projections retain Dart types -04:24 +592 -2: test/union_test.dart: union sqlite Dart maps and relation selections are rejected before execution -04:24 +593 -2: test/union_test.dart: union sqlite codecs and nullability cannot silently change across operands -04:24 +594 -2: test/union_test.dart: union sqlite shared domain codecs work, and nullable wrappers retain decoding semantics -04:24 +595 -2: test/union_test.dart: union sqlite export references and outer join nullability are guarded -04:24 +596 -2: test/union_test.dart: union sqlite compound mutations and mixed sessions fail before SQL -04:24 +597 -2: test/union_test.dart: union sqlite all record arities use typed SQL decoding -04:24 +598 -2: test/union_test.dart: union sqlite subscription observes committed changes in both operands -04:24 +599 -2: test/union_test.dart: union postgres SQL duplicate and NULL semantics use one statement -04:24 +600 -2: test/union_test.dart: union postgres heterogeneous records map only after SQL union and final pagination -04:24 +601 -2: test/union_test.dart: union postgres operand limits, order, parameters and final filter stay scoped -04:24 +602 -2: test/union_test.dart: union postgres repeated SQL expressions preserve positional columns -04:24 +603 -2: test/union_test.dart: union postgres nested sets preserve association, CTE exports and streaming -04:24 +604 -2: test/union_test.dart: union postgres same CTE names in distinct operands stay in separate scopes -04:25 +605 -2: test/union_test.dart: union postgres SQL scalar, IN subqueries, aggregate and zero-row results compose -04:25 +606 -2: test/union_test.dart: union postgres grouped branches and window branches remain SQL queries -04:25 +607 -2: test/union_test.dart: union postgres bound constant projections retain Dart types -04:25 +608 -2: test/union_test.dart: union postgres Dart maps and relation selections are rejected before execution -04:25 +609 -2: test/union_test.dart: union postgres codecs and nullability cannot silently change across operands -04:25 +610 -2: test/union_test.dart: union postgres shared domain codecs work, and nullable wrappers retain decoding semantics -04:25 +611 -2: test/union_test.dart: union postgres export references and outer join nullability are guarded -04:25 +612 -2: test/union_test.dart: union postgres compound mutations and mixed sessions fail before SQL -04:25 +613 -2: test/union_test.dart: union postgres all record arities use typed SQL decoding -04:25 +614 -2: test/union_test.dart: union postgres subscription observes committed changes in both operands -04:25 +615 -2: loading test/import_cli_test.dart -04:25 +615 -2: test/import_cli_test.dart: CLI sqlite imports reviews generates and baselines existing data without replacing files -05:12 +616 -2: test/import_cli_test.dart: CLI postgres imports reviews generates and baselines existing data without replacing files -05:55 +617 -2: loading test/decimal_division_test.dart -05:55 +617 -2: test/decimal_division_test.dart: decimal division sqlite signed modes and scales match exact integer arithmetic -05:55 +618 -2: test/decimal_division_test.dart: decimal division sqlite exact defaults reject lost digits and zero divisors, NULL propagates -05:55 +619 -2: test/decimal_division_test.dart: decimal division sqlite precision exceeds native division scale without double rounding -05:55 +620 -2: test/decimal_division_test.dart: decimal division sqlite huge remainders avoid overflowing intermediate scaled products -06:17 +621 -2: test/decimal_division_test.dart: decimal division sqlite outermost integer scale and result overflow are explicit -06:18 +622 -2: test/decimal_division_test.dart: decimal division sqlite aggregates windows CTEs predicates and unions compose -06:18 +623 -2: test/decimal_division_test.dart: decimal division sqlite expression assignments and transaction rollback preserve values -06:18 +624 -2: test/decimal_division_test.dart: decimal division sqlite window filtering ordering DISTINCT and pagination keep their SQL order -06:18 +625 -2: test/decimal_division_test.dart: decimal division sqlite windows over grouped aggregates run after HAVING -06:18 +626 -2: test/decimal_division_test.dart: decimal division sqlite window results compose through CTEs sets and correlated subqueries -06:18 +627 -2: test/decimal_division_test.dart: decimal division sqlite batched relations retain per-parent window pagination -06:18 +628 -2: test/decimal_division_test.dart: decimal division postgres signed modes and scales match exact integer arithmetic -06:18 +629 -2: test/decimal_division_test.dart: decimal division postgres exact defaults reject lost digits and zero divisors, NULL propagates -06:18 +630 -2: test/decimal_division_test.dart: decimal division postgres precision exceeds native division scale without double rounding -06:18 +631 -2: test/decimal_division_test.dart: decimal division postgres huge remainders avoid overflowing intermediate scaled products -06:38 +632 -2: test/decimal_division_test.dart: decimal division postgres outermost integer scale and result overflow are explicit -06:38 +633 -2: test/decimal_division_test.dart: decimal division postgres aggregates windows CTEs predicates and unions compose -06:38 +634 -2: test/decimal_division_test.dart: decimal division postgres expression assignments and transaction rollback preserve values -06:38 +635 -2: test/decimal_division_test.dart: decimal division postgres window filtering ordering DISTINCT and pagination keep their SQL order -06:39 +636 -2: test/decimal_division_test.dart: decimal division postgres windows over grouped aggregates run after HAVING -06:39 +637 -2: test/decimal_division_test.dart: decimal division postgres window results compose through CTEs sets and correlated subqueries -06:39 +638 -2: test/decimal_division_test.dart: decimal division postgres batched relations retain per-parent window pagination -06:39 +639 -2: test/decimal_division_test.dart: decimal division postgres volatile operands are evaluated exactly once each -06:39 +640 -2: loading test/cli_test.dart -06:39 +640 -2: test/cli_test.dart: Dart CLI generates, reviews, upgrades and verifies constraints without overwriting history -07:48 +641 -2: test/cli_test.dart: Dart CLI baselines existing rows; read-only connections and invalid options reject writes -08:35 +642 -2: test/cli_test.dart: Dart PostgreSQL configuration supports checked nontransactional recovery operations -09:28 +643 -2: loading test/decimal_test.dart -09:28 +643 -2: test/decimal_test.dart: exact decimal sqlite generated writes, defaults, patches and projections preserve all digits -09:28 +644 -2: test/decimal_test.dart: exact decimal sqlite numeric ordering, predicates, IN, distinct and stable cursors -09:28 +645 -2: test/decimal_test.dart: exact decimal sqlite sums, min/max, grouped aggregates and sliding windows stay exact -09:28 +646 -2: test/decimal_test.dart: exact decimal sqlite computed CTEs, union results, subqueries and streaming keep decimal semantics -09:28 +647 -2: test/decimal_test.dart: exact decimal sqlite numeric primary keys, foreign keys, joined and batched relations -09:28 +648 -2: test/decimal_test.dart: exact decimal sqlite snapshots, catalog verification and imported declarations retain decimal storage -09:30 +649 -2: test/decimal_test.dart: exact decimal sqlite aggregate intermediate overflow may cancel before a valid final result -09:30 +650 -2: test/decimal_test.dart: exact decimal sqlite historical decimal keys resume backfills without lexical ordering or precision loss -09:30 +651 -2: test/decimal_test.dart: exact decimal sqlite reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:30 +652 -2: test/decimal_test.dart: exact decimal sqlite invalid external values fail decoding; later valid queries still work -09:30 +653 -2: test/decimal_test.dart: exact decimal postgres generated writes, defaults, patches and projections preserve all digits -09:30 +654 -2: test/decimal_test.dart: exact decimal postgres numeric ordering, predicates, IN, distinct and stable cursors -09:30 +655 -2: test/decimal_test.dart: exact decimal postgres sums, min/max, grouped aggregates and sliding windows stay exact -09:30 +656 -2: test/decimal_test.dart: exact decimal postgres computed CTEs, union results, subqueries and streaming keep decimal semantics -09:30 +657 -2: test/decimal_test.dart: exact decimal postgres numeric primary keys, foreign keys, joined and batched relations -09:30 +658 -2: test/decimal_test.dart: exact decimal postgres snapshots, catalog verification and imported declarations retain decimal storage -09:31 +659 -2: test/decimal_test.dart: exact decimal postgres aggregate intermediate overflow may cancel before a valid final result -09:31 +660 -2: test/decimal_test.dart: exact decimal postgres historical decimal keys resume backfills without lexical ordering or precision loss -09:31 +661 -2: test/decimal_test.dart: exact decimal postgres reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -09:31 +662 -2: test/decimal_test.dart: exact decimal postgres invalid external values fail decoding; later valid queries still work -09:31 +663 -2: test/decimal_test.dart: decimal SQL rejects drivers without exact operations before execution -09:31 +664 -2: test/decimal_test.dart: SQLite recognizes only column decimal collations, and detects drift -09:31 +665 -2: loading test/watch_test.dart -09:31 +665 -2: test/watch_test.dart: independent native SQLite workers require explicit notification -09:31 +666 -2: test/watch_test.dart: watch sqlite lazy initial snapshot and relevant table invalidation -09:31 +667 -2: test/watch_test.dart: watch sqlite transactions publish one snapshot after commit and none on rollback -09:31 +668 -2: test/watch_test.dart: watch sqlite savepoint changes merge on release and disappear on rollback -09:31 +669 -2: test/watch_test.dart: watch sqlite failed statements, zero-row writes and conflict no-ops do not refresh -09:31 +670 -2: test/watch_test.dart: watch sqlite batch writes and upsert returning publish committed changes -09:31 +671 -2: test/watch_test.dart: watch sqlite joined and nested batch relations track child writes -09:31 +672 -2: test/watch_test.dart: watch sqlite relation predicates, SQL subqueries and CTE sources are dependencies -09:31 +673 -2: test/watch_test.dart: watch sqlite declared cascade deletion invalidates a child-only query -09:31 +674 -2: test/watch_test.dart: watch sqlite CASCADE tracks declared transitive deletion effects -09:31 +675 -2: test/watch_test.dart: watch sqlite SET NULL tracks declared transitive deletion effects -09:31 +676 -2: test/watch_test.dart: watch sqlite explicit JOINs and joined CTE definitions track both physical sources -09:31 +677 -2: test/watch_test.dart: watch sqlite raw SQL declares changed and read tables; explicit invalidation follows transactions -09:31 +678 -2: test/watch_test.dart: watch sqlite paused listeners coalesce writes and stop fetching until resumed -09:31 +679 -2: test/watch_test.dart: watch sqlite invalidation during a read suppresses the stale snapshot and refetches -09:31 +680 -2: test/watch_test.dart: watch sqlite decoding errors are reported and a later write can recover -09:31 +681 -2: test/watch_test.dart: watch sqlite separate subscriptions and views of one driver receive changes -09:31 +682 -2: test/watch_test.dart: watch sqlite watching leased sessions is rejected and cancelled tokens close the stream -09:31 +683 -2: test/watch_test.dart: watch sqlite database close finishes watchers even when a listener is paused -09:31 +684 -2: test/watch_test.dart: watch sqlite cancelling an active watched query interrupts SQL and releases its lease -09:31 +685 -2: test/watch_test.dart: watch postgres lazy initial snapshot and relevant table invalidation -09:31 +686 -2: test/watch_test.dart: watch postgres transactions publish one snapshot after commit and none on rollback -09:31 +687 -2: test/watch_test.dart: watch postgres savepoint changes merge on release and disappear on rollback -09:31 +688 -2: test/watch_test.dart: watch postgres failed statements, zero-row writes and conflict no-ops do not refresh -09:31 +689 -2: test/watch_test.dart: watch postgres batch writes and upsert returning publish committed changes -09:31 +690 -2: test/watch_test.dart: watch postgres joined and nested batch relations track child writes -09:31 +691 -2: test/watch_test.dart: watch postgres relation predicates, SQL subqueries and CTE sources are dependencies -09:31 +692 -2: test/watch_test.dart: watch postgres declared cascade deletion invalidates a child-only query -09:31 +693 -2: test/watch_test.dart: watch postgres CASCADE tracks declared transitive deletion effects -09:31 +694 -2: test/watch_test.dart: watch postgres SET NULL tracks declared transitive deletion effects -09:31 +695 -2: test/watch_test.dart: watch postgres explicit JOINs and joined CTE definitions track both physical sources -09:31 +696 -2: test/watch_test.dart: watch postgres raw SQL declares changed and read tables; explicit invalidation follows transactions -09:32 +697 -2: test/watch_test.dart: watch postgres paused listeners coalesce writes and stop fetching until resumed -09:32 +698 -2: test/watch_test.dart: watch postgres invalidation during a read suppresses the stale snapshot and refetches -09:32 +699 -2: test/watch_test.dart: watch postgres decoding errors are reported and a later write can recover -09:32 +700 -2: test/watch_test.dart: watch postgres separate subscriptions and views of one driver receive changes -09:32 +701 -2: test/watch_test.dart: watch postgres watching leased sessions is rejected and cancelled tokens close the stream -09:32 +702 -2: test/watch_test.dart: watch postgres database close finishes watchers even when a listener is paused -09:32 +703 -2: test/watch_test.dart: watch postgres cancelling an active watched query interrupts SQL and releases its lease -09:32 +704 -2: test/watch_test.dart: watch postgres lost commit acknowledgement triggers a conservative re-read -09:32 +705 -2: test/watch_test.dart: watch postgres independent connections require an explicit external-write notification -09:32 +706 -2: loading test/backfill_test.dart -09:32 +706 -2: test/backfill_test.dart: backfill sqlite chunks pause and resume from a checksummed historical declaration -09:32 +707 -2: test/backfill_test.dart: backfill sqlite failed chunk rolls back both writes and cursor, then resumes after repair -09:32 +708 -2: test/backfill_test.dart: backfill sqlite failed completion proof preserves finished chunks without replay -09:32 +709 -2: test/backfill_test.dart: backfill sqlite frozen upper bound and completion proof detect incompatible new writes -09:32 +710 -2: test/backfill_test.dart: backfill sqlite competing runners share committed chunks without duplicate updates -09:32 +711 -2: test/backfill_test.dart: backfill sqlite lost chunk commit acknowledgement preserves the committed frontier -09:32 +712 -2: test/backfill_test.dart: backfill sqlite old checkpoint tables upgrade on apply; completed reads remain nonmutating -09:32 +713 -2: test/backfill_test.dart: backfill sqlite completion conditions reject numeric results instead of truthiness -09:32 +714 -2: test/backfill_test.dart: backfill sqlite empty input still requires the completion condition -09:32 +715 -2: test/backfill_test.dart: backfill sqlite later ordinary migrations retain group atomicity after a completed backfill -09:32 +716 -2: test/backfill_test.dart: backfill sqlite AFTER triggers cannot silently move keys past a committed cursor -09:32 +717 -2: test/backfill_test.dart: backfill sqlite corrupt checkpoint dimensions and invalid run limits are rejected -09:32 +718 -2: test/backfill_test.dart: backfill sqlite rebuild, chunked backfill and required-column rebuild preserve FK enforcement -09:32 +719 -2: test/backfill_test.dart: backfill sqlite CLI previews non-atomic work and reports a bounded run as incomplete -09:58 +720 -2: test/backfill_test.dart: backfill sqlite temporary tables cannot redirect the historical backfill target -09:58 +721 -2: test/backfill_test.dart: backfill sqlite temporary objects cannot impersonate durable migration metadata -09:58 +722 -2: test/backfill_test.dart: backfill sqlite committed chunks invalidate relevant typed query subscriptions -09:58 +723 -2: test/backfill_test.dart: backfill sqlite real process exit after second chunk write resumes correctly -10:20 +724 -2: test/backfill_test.dart: backfill sqlite real process exit after second chunk commit resumes correctly -10:42 +725 -2: test/backfill_test.dart: backfill postgres chunks pause and resume from a checksummed historical declaration -10:42 +726 -2: test/backfill_test.dart: backfill postgres failed chunk rolls back both writes and cursor, then resumes after repair -10:42 +727 -2: test/backfill_test.dart: backfill postgres failed completion proof preserves finished chunks without replay -10:42 +728 -2: test/backfill_test.dart: backfill postgres frozen upper bound and completion proof detect incompatible new writes -10:42 +729 -2: test/backfill_test.dart: backfill postgres competing runners share committed chunks without duplicate updates -10:42 +730 -2: test/backfill_test.dart: backfill postgres lost chunk commit acknowledgement preserves the committed frontier -10:42 +731 -2: test/backfill_test.dart: backfill postgres old checkpoint tables upgrade on apply; completed reads remain nonmutating -10:42 +732 -2: test/backfill_test.dart: backfill postgres completion conditions reject numeric results instead of truthiness -10:42 +733 -2: test/backfill_test.dart: backfill postgres empty input still requires the completion condition -10:42 +734 -2: test/backfill_test.dart: backfill postgres later ordinary migrations retain group atomicity after a completed backfill -10:42 +735 -2: test/backfill_test.dart: backfill postgres AFTER triggers cannot silently move keys past a committed cursor -10:42 +736 -2: test/backfill_test.dart: backfill postgres corrupt checkpoint dimensions and invalid run limits are rejected -10:42 +737 -2: test/backfill_test.dart: backfill postgres CLI previews non-atomic work and reports a bounded run as incomplete -11:10 +738 -2: test/backfill_test.dart: backfill postgres temporary tables cannot redirect the historical backfill target -11:10 +739 -2: test/backfill_test.dart: backfill postgres temporary objects cannot impersonate durable migration metadata -11:10 +740 -2: test/backfill_test.dart: backfill postgres row security cannot hide unfinished rows from the completion proof -11:10 +741 -2: test/backfill_test.dart: backfill postgres inherited duplicate primary keys cannot advance a cursor -11:10 +742 -2: test/backfill_test.dart: backfill postgres committed chunks invalidate relevant typed query subscriptions -11:10 +743 -2: test/backfill_test.dart: backfill postgres real process exit after second chunk write resumes correctly -11:25 +744 -2: test/backfill_test.dart: backfill postgres real process exit after second chunk commit resumes correctly -11:43 +745 -2: loading test/custom_codec_test.dart -11:43 +745 -2: test/custom_codec_test.dart: (setUpAll) -11:43 +745 -2: test/custom_codec_test.dart: custom codecs sqlite generated custom values round trip through create, select and patch -11:43 +746 -2: test/custom_codec_test.dart: custom codecs sqlite enum labels are stable text and unknown labels fail decoding -11:43 +747 -2: test/custom_codec_test.dart: custom codecs sqlite domain validation and bound parameters apply to writes and predicates -11:43 +748 -2: test/custom_codec_test.dart: custom codecs sqlite JSON documents distinguish SQL NULL, JSON null and string scalars -11:43 +749 -2: test/custom_codec_test.dart: custom codecs sqlite nullable structured codec rejects JSON null instead of masking invalid data -11:43 +750 -2: test/custom_codec_test.dart: custom codecs sqlite custom IDs and domain projections work across joined and batched relations -11:43 +751 -2: test/custom_codec_test.dart: custom codecs sqlite custom values participate in atomic batch writes and native upsert -11:43 +752 -2: test/custom_codec_test.dart: custom codecs sqlite custom IDs and enums survive cursor transport and streaming decoders -11:43 +753 -2: test/custom_codec_test.dart: custom codecs sqlite migration snapshots retain physical storage without executing domain codecs -11:43 +754 -2: test/custom_codec_test.dart: custom codecs postgres generated custom values round trip through create, select and patch -11:43 +755 -2: test/custom_codec_test.dart: custom codecs postgres enum labels are stable text and unknown labels fail decoding -11:43 +756 -2: test/custom_codec_test.dart: custom codecs postgres domain validation and bound parameters apply to writes and predicates -11:43 +757 -2: test/custom_codec_test.dart: custom codecs postgres JSON documents distinguish SQL NULL, JSON null and string scalars -11:43 +758 -2: test/custom_codec_test.dart: custom codecs postgres nullable structured codec rejects JSON null instead of masking invalid data -11:44 +759 -2: test/custom_codec_test.dart: custom codecs postgres custom IDs and domain projections work across joined and batched relations -11:44 +760 -2: test/custom_codec_test.dart: custom codecs postgres custom values participate in atomic batch writes and native upsert -11:44 +761 -2: test/custom_codec_test.dart: custom codecs postgres custom IDs and enums survive cursor transport and streaming decoders -11:44 +762 -2: test/custom_codec_test.dart: custom codecs postgres migration snapshots retain physical storage without executing domain codecs -11:44 +763 -2: test/custom_codec_test.dart: (tearDownAll) -11:44 +763 -2: loading test/plan_test.dart -11:44 +763 -2: test/plan_test.dart: inspect sqlite reports composite key width and validates each potential batch -11:44 +764 -2: test/plan_test.dart: inspect sqlite compiles nested plans without acquiring or decoding -11:44 +765 -2: test/plan_test.dart: inspect sqlite distinguishes same-statement joins and skipped batches -11:44 +766 -2: test/plan_test.dart: inspect sqlite retains CTE, UNION and raw SQL boundaries -11:44 +767 -2: test/plan_test.dart: plan execution sqlite one-key SQL templates match executed SQL and internal slots -11:44 +768 -2: test/plan_test.dart: inspect postgres reports composite key width and validates each potential batch -11:44 +769 -2: test/plan_test.dart: inspect postgres compiles nested plans without acquiring or decoding -11:44 +770 -2: test/plan_test.dart: inspect postgres distinguishes same-statement joins and skipped batches -11:44 +771 -2: test/plan_test.dart: inspect postgres retains CTE, UNION and raw SQL boundaries -11:44 +772 -2: test/plan_test.dart: plan execution postgres one-key SQL templates match executed SQL and internal slots -11:44 +773 -2: loading test/check_test.dart -11:44 +773 -2: test/check_test.dart: CHECK sqlite generated named and unnamed constraints enforce values and permit NULL -11:44 +774 -2: test/check_test.dart: CHECK sqlite import preserves names and SQL then generates a matching snapshot -11:46 +775 -2: test/check_test.dart: CHECK sqlite add, change and remove constraints validate old rows atomically -11:46 +776 -2: test/check_test.dart: CHECK sqlite duplicate unnamed checks consume and drop exactly one catalog entry -11:46 +777 -2: test/check_test.dart: CHECK sqlite explicit checked renames preserve rows, incoming references, indexes and views -11:46 +778 -2: test/check_test.dart: CHECK sqlite verification preserves casts, precedence, literals and trailing comments -11:46 +779 -2: test/check_test.dart: CHECK sqlite quoted Unicode identifiers retain SQLite case distinctions -11:46 +780 -2: test/check_test.dart: CHECK sqlite undeclared CHECK is refused before a rebuild and remains enforced -11:46 +781 -2: test/check_test.dart: CHECK postgres generated named and unnamed constraints enforce values and permit NULL -11:46 +782 -2: test/check_test.dart: CHECK postgres import preserves names and SQL then generates a matching snapshot -11:47 +783 -2: test/check_test.dart: CHECK postgres add, change and remove constraints validate old rows atomically -11:47 +784 -2: test/check_test.dart: CHECK postgres duplicate unnamed checks consume and drop exactly one catalog entry -11:47 +785 -2: test/check_test.dart: CHECK postgres explicit checked renames preserve rows, incoming references, indexes and views -11:47 +786 -2: test/check_test.dart: CHECK postgres verification preserves casts, precedence, literals and trailing comments -11:47 +787 -2: test/check_test.dart: CHECK postgres catalog excludes unvalidated and NO INHERIT constraints -11:47 +788 -2: test/check_test.dart: CHECK postgres verification plans but does not execute volatile row expressions -11:47 +789 -2: test/check_test.dart: dialect overrides only alter the affected backend and preserve old snapshot JSON -11:47 +790 -2: loading test/temporal_test.dart -11:47 +790 -2: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: false -11:47 +791 -2: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: true -11:47 +792 -2: test/temporal_test.dart: Gregorian cycles, era boundaries and full finite date range -11:47 +793 -2: test/temporal_test.dart: local time precision, day endpoint and timestamp arithmetic -11:47 +794 -2: test/temporal_test.dart: PostgreSQL binary registry covers boundaries without DateTime -11:47 +795 -2: test/temporal_test.dart: local temporal sqlite generated writes, defaults, null patches and typed parameters -11:47 +796 -2: test/temporal_test.dart: local temporal sqlite finite extremes and microseconds survive native rows and streams -11:47 +797 -2: test/temporal_test.dart: local temporal sqlite calendar ordering, min/max, grouping and stable date cursors -11:47 +798 -2: test/temporal_test.dart: local temporal sqlite equivalent date keys drive batched relations and unique indexes -11:47 +799 -2: test/temporal_test.dart: local temporal sqlite time and local timestamp comparisons retain calendar semantics -11:47 +800 -2: test/temporal_test.dart: local temporal sqlite reviewed text-to-time migration rolls back duplicate clock values -11:47 +801 -2: test/temporal_test.dart: local temporal sqlite invalid external dates fail typed reads without damaging the connection -11:47 +802 -2: test/temporal_test.dart: local temporal sqlite snapshots, catalog import and regenerated defaults agree -11:49 +803 -2: test/temporal_test.dart: local temporal sqlite historical date keys resume a bounded backfill in calendar order -11:49 +804 -2: test/temporal_test.dart: local temporal postgres catalog retains time-truncating casts in expression defaults -11:49 +805 -2: test/temporal_test.dart: local temporal postgres generated writes, defaults, null patches and typed parameters -11:49 +806 -2: test/temporal_test.dart: local temporal postgres finite extremes and microseconds survive native rows and streams -11:49 +807 -2: test/temporal_test.dart: local temporal postgres calendar ordering, min/max, grouping and stable date cursors -11:49 +808 -2: test/temporal_test.dart: local temporal postgres equivalent date keys drive batched relations and unique indexes -11:50 +809 -2: test/temporal_test.dart: local temporal postgres time and local timestamp comparisons retain calendar semantics -11:50 +810 -2: test/temporal_test.dart: local temporal postgres reviewed text-to-time migration rolls back duplicate clock values -11:50 +811 -2: test/temporal_test.dart: local temporal postgres invalid external dates fail typed reads without damaging the connection -11:50 +812 -2: test/temporal_test.dart: local temporal postgres snapshots, catalog import and regenerated defaults agree -11:50 +813 -2: test/temporal_test.dart: local temporal postgres historical date keys resume a bounded backfill in calendar order -11:50 +814 -2: loading test/unconstrained_relation_test.dart -11:50 +814 -2: test/unconstrained_relation_test.dart: unconstrained sqlite catalogs contain no FK, implicit index or uniqueness promise -11:50 +815 -2: test/unconstrained_relation_test.dart: unconstrained sqlite adding and removing a real FK remains a reviewed physical migration -11:50 +816 -2: test/unconstrained_relation_test.dart: unconstrained sqlite unique joins preserve root rows, NULL components and missing targets -11:50 +817 -2: test/unconstrained_relation_test.dart: unconstrained sqlite required target and filter visibility remain explicit -11:50 +818 -2: test/unconstrained_relation_test.dart: unconstrained sqlite nonunique composite collections batch by tuple and page per parent -11:50 +819 -2: test/unconstrained_relation_test.dart: unconstrained sqlite to-one uses cardinality checks without assuming a unique lookup -11:50 +820 -2: test/unconstrained_relation_test.dart: unconstrained sqlite correlated filters and counts do not materialize related rows -11:50 +821 -2: test/unconstrained_relation_test.dart: unconstrained sqlite self relations and inverse collections share the existing planner -11:50 +822 -2: test/unconstrained_relation_test.dart: unconstrained sqlite delete has no cascade and watches track the joined target -11:50 +823 -2: test/unconstrained_relation_test.dart: unconstrained sqlite transaction reads observe writes and outer rollback restores targets -11:50 +824 -2: test/unconstrained_relation_test.dart: unconstrained sqlite streaming preserves optional and batched relation result shapes -11:50 +825 -2: test/unconstrained_relation_test.dart: unconstrained sqlite floating relation keys retain storage intent and key equality -11:50 +826 -2: test/unconstrained_relation_test.dart: unconstrained postgres catalogs contain no FK, implicit index or uniqueness promise -11:51 +827 -2: test/unconstrained_relation_test.dart: unconstrained postgres adding and removing a real FK remains a reviewed physical migration -11:51 +828 -2: test/unconstrained_relation_test.dart: unconstrained postgres unique joins preserve root rows, NULL components and missing targets -11:51 +829 -2: test/unconstrained_relation_test.dart: unconstrained postgres required target and filter visibility remain explicit -11:51 +830 -2: test/unconstrained_relation_test.dart: unconstrained postgres nonunique composite collections batch by tuple and page per parent -11:51 +831 -2: test/unconstrained_relation_test.dart: unconstrained postgres to-one uses cardinality checks without assuming a unique lookup -11:51 +832 -2: test/unconstrained_relation_test.dart: unconstrained postgres correlated filters and counts do not materialize related rows -11:51 +833 -2: test/unconstrained_relation_test.dart: unconstrained postgres self relations and inverse collections share the existing planner -11:51 +834 -2: test/unconstrained_relation_test.dart: unconstrained postgres delete has no cascade and watches track the joined target -11:51 +835 -2: test/unconstrained_relation_test.dart: unconstrained postgres transaction reads observe writes and outer rollback restores targets -11:51 +836 -2: test/unconstrained_relation_test.dart: unconstrained postgres streaming preserves optional and batched relation result shapes -11:51 +837 -2: test/unconstrained_relation_test.dart: unconstrained postgres floating relation keys retain storage intent and key equality -11:51 +838 -2: loading test/decimal_value_test.dart -11:51 +838 -2: test/decimal_value_test.dart: normalization, scientific notation, equality and hash are exact -11:51 +839 -2: test/decimal_value_test.dart: invalid and approximate inputs fail instead of silently rounding -11:51 +840 -2: test/decimal_value_test.dart: range boundaries are checked without binary numeric conversions -11:51 +841 -2: test/decimal_value_test.dart: addition, subtraction, multiplication and ordering keep every digit -11:51 +842 -2: test/decimal_value_test.dart: division requires an explicit scale and rejects implicit rounding -11:51 +843 -2: test/decimal_value_test.dart: rounding modes handle negative values, ties and negative scales -11:51 +844 -2: test/decimal_value_test.dart: small fixed-scale arithmetic agrees with independent integer arithmetic -11:51 +845 -2: Some tests failed. - -Failing tests: - test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay - test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay - -Consider enabling the flag chain-stack-traces to receive more detailed exceptions. -For example, 'dart test --chain-stack-traces'. diff --git a/research/validation/migration-targets.md b/research/validation/migration-targets.md deleted file mode 100644 index 008c320b..00000000 --- a/research/validation/migration-targets.md +++ /dev/null @@ -1,70 +0,0 @@ -# Single-engine migration validation - -Captured 2026-09-15T14:19:28.276397+00:00. Dart 3.13.3, -macOS arm64, native SQLite through sqlite3 3.6.0, and a disposable PostgreSQL 18.4 -instance. The instance and test directories were owned by this run and cleaned up. - -Runtime source digest: `a3c1161cff2726a66456095a2cbf86f3154cc99aa0a38d860c3e333f466a332e`. SHA-256 over lexically sorted -Dart paths under `lib`/`bin` plus `pubspec.yaml`/`pubspec.lock`, each encoded as -UTF-8 path, NUL, file bytes, NUL. Runtime sources were unchanged during these runs. - -| Validation | Result | Evidence | -| --- | --- | --- | -| Complete native suite with `ORM_TEST_POSTGRES` set | 845 passed, 2 failed, 11:51 | [Full log](migration-targets-full.log) | -| Migration target and PostgreSQL recovery suites after fixing fixture configuration | 21 passed, 00:19, exit 0 | [Follow-up](migration-targets-followup.log) | -| Consumer migration source roundtrip and standalone AOT bundle after deleting consumer sources/assets | Passed in full run | `migration_source_test.dart` in full log | -| Statically registered historical backfill in native AOT | Pause, concurrent resume, exactly-once updates, startup compatibility passed | [AOT log](migration-targets-backfill-aot.log) | -| Real Chrome JavaScript | 19 checks passed | [JS log](migration-targets-browser-js.log) | -| Real Chrome Dart WASM | 19 checks passed | [WASM log](migration-targets-browser-wasm.log) | -| Public registry CLI and example entrypoint | Target initialization/preservation/rejected switch; check/apply/verify/no replay passed | [CLI log](migration-targets-cli.log) | -| `dart analyze` and `git diff --check` | No issues | Final working tree | - -The full run and follow-up are separate invocations. The two failures were real -process-exit tests in `migration_recovery_test.dart`: their test project used the -SQLite registry default for PostgreSQL migration files. The new target check -rejected them before execution. The fixture now initializes a PostgreSQL registry; -both concurrent-DDL and transaction-commit exit/recovery cases pass in the follow-up. -The follow-up also covers the final empty-history connection rejection and unused -empty-expression assertions. This is not a claim of a single all-green full run. - -Commands (Dart binary from the verified SDK; local `DEVELOPER_DIR` points to the -installed CommandLineTools): - -```sh -ORM_TEST_POSTGRES= dart test --reporter expanded -ORM_TEST_POSTGRES= dart test test/migration_target_test.dart test/migration_recovery_test.dart --reporter expanded -dart run tool/test_browser.dart -dart run tool/test_browser.dart --wasm -dart build cli --target test/support/native_backfill.dart --output .dart_tool/target-backfill-aot -.dart_tool/target-backfill-aot/bundle/bin/native_backfill -dart analyze -``` - -The CLI smoke initialized a temporary registry using -`dart run bin/orm.dart migration registry --dialect postgres`, rebuilt -it without changing its target, and verified a SQLite retarget failed without -changing its source. `example/migrate.dart check`, `apply`, `verify`, `apply` used -an isolated `ORM_SQLITE_PATH`; the final apply returned an empty list. - -Covered boundaries: - -- A migration has one engine and one step list. Engine identity is checksummed; - mixed histories and mismatched connections fail before migration SQL. -- Empty histories retain a target. Registry rebuilding preserves it and neither - rewrites fixed fingerprints nor accepts another engine. -- Only selected CHECK/computed SQL enters frozen history. Unused overrides cannot - change the plan, fingerprint or emitted migration source. -- SQLite virtual-column indexes, stored/virtual transitions and materialization - work without PostgreSQL restrictions. Replacing ordinary values with computed - values requires explicit destructive-change review. -- PostgreSQL uses its own conversion expression, constraint operations, concurrent - indexes and recovery logic. SQLite retains rebuild/foreign-key/rollback behavior. -- PostgreSQL execution checks server version before locks or journal changes. - Rejection of a 17.x response is a driver-double unit check; real PostgreSQL - execution was verified on 18.4, not on an installed 17.x server. - -No Android/Apple-device/Flutter runtime was rerun for this correction. The Flutter -example's Dart migration history and shared runtime were updated; the earlier -Android capture applies to its recorded revision. There is no cross-engine data -transfer or distributed transaction guarantee. Reviewed SQL, deployment downtime -and database objects outside the modeled schema still require application review. diff --git a/research/validation/native.json b/research/validation/native.json deleted file mode 100644 index 7c8b9281..00000000 --- a/research/validation/native.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "capturedAt": "2026-09-15T10:04:34.415029+00:00", - "sourceBase": "20ffee7", - "command": "ORM_TEST_POSTGRES= dart test -r expanded", - "exitCode": 0, - "passed": 835, - "elapsed": "07:44", - "dart": "3.13.3", - "postgres": "18.4", - "sqlite": "3.53.4", - "log": "research/validation/native.log", - "logSha256": "3fa6b4160ba28d46fa9ea1937b95cdca562659307b61f805d36fd6e780255b71", - "sources": { - "lib/src/migrate/catalog.dart": "80c1a396727d57e9c49e37914c076e6fab64a3eb43437b522c82c749cf280359", - "test/import_test.dart": "3627b4e57c7493a1757e90529942c02b2554add29c7f128d3a63708e446a5942", - "pubspec.yaml": "0a6bfe003864c3599c11b4acc208e611af71135904bea531edac67760bc559be", - "pubspec.lock": "a17ed225d38e69c125be83a4a60d5574e61190408e33b191b23de735d8af5416", - "dart_test.yaml": "fb685b478e815629ba593e1d74975bc2aabc50293bbd27cb30f62f7e4ae8f5ab" - }, - "scope": "Complete native test suite, serialized on macOS; real SQLite and PostgreSQL are enabled. Includes generator/CLI/build_runner processes, transactions/streaming, migrations/recovery, catalog/import and type checks. Browser and Flutter are separate captures." -} diff --git a/research/validation/native.log b/research/validation/native.log deleted file mode 100644 index 99f4e272..00000000 --- a/research/validation/native.log +++ /dev/null @@ -1,889 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/client_default_test.dart -00:00 +0: test/client_default_test.dart: client defaults sqlite omitted values use typed Dart factories and return stored records -00:00 +1: test/client_default_test.dart: client defaults sqlite explicit values, null and SQL DEFAULT bypass their client factory -00:00 +2: test/client_default_test.dart: client defaults sqlite prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +3: test/client_default_test.dart: client defaults sqlite batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +4: test/client_default_test.dart: client defaults sqlite updates and conflict updates do not reset omitted client values -00:00 +5: test/client_default_test.dart: client defaults sqlite factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +6: test/client_default_test.dart: client defaults sqlite snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +7: test/client_default_test.dart: client defaults sqlite explicit DEFAULT can choose database identity instead of the client value -00:00 +8: test/client_default_test.dart: client defaults postgres omitted values use typed Dart factories and return stored records -00:00 +9: test/client_default_test.dart: client defaults postgres explicit values, null and SQL DEFAULT bypass their client factory -00:00 +10: test/client_default_test.dart: client defaults postgres prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +11: test/client_default_test.dart: client defaults postgres batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +12: test/client_default_test.dart: client defaults postgres updates and conflict updates do not reset omitted client values -00:00 +13: test/client_default_test.dart: client defaults postgres factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +14: test/client_default_test.dart: client defaults postgres snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +15: test/client_default_test.dart: client defaults postgres explicit DEFAULT can choose database identity instead of the client value -00:00 +16: test/client_default_test.dart: generation reads factory references without executing them and supports moved output -00:02 +17: loading test/decimal_precision_test.dart -00:02 +17: test/decimal_precision_test.dart: decimal precision sqlite column coercion rounds signed ties and handles negative and excess scales -00:02 +18: test/decimal_precision_test.dart: decimal precision sqlite writes constrain columns while arithmetic and wider aggregates retain their result precision -00:02 +19: test/decimal_precision_test.dart: decimal precision sqlite batch inserts and upserts use column coercion and roll back as a unit -00:02 +20: test/decimal_precision_test.dart: decimal precision sqlite catalogs snapshots and imported declarations retain precision scale and defaults -00:04 +21: test/decimal_precision_test.dart: decimal precision sqlite reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:04 +22: test/decimal_precision_test.dart: decimal precision sqlite physical renames and changing constrained defaults preserve column contracts -00:04 +23: test/decimal_precision_test.dart: decimal precision sqlite maximum declared precision and scale bounds remain exact -00:04 +24: test/decimal_precision_test.dart: decimal precision sqlite historical backfills retain constrained decimal key declarations -00:04 +25: test/decimal_precision_test.dart: decimal precision postgres column coercion rounds signed ties and handles negative and excess scales -00:04 +26: test/decimal_precision_test.dart: decimal precision postgres writes constrain columns while arithmetic and wider aggregates retain their result precision -00:04 +27: test/decimal_precision_test.dart: decimal precision postgres batch inserts and upserts use column coercion and roll back as a unit -00:04 +28: test/decimal_precision_test.dart: decimal precision postgres catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +29: test/decimal_precision_test.dart: decimal precision postgres reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:05 +30: test/decimal_precision_test.dart: decimal precision postgres physical renames and changing constrained defaults preserve column contracts -00:05 +31: test/decimal_precision_test.dart: decimal precision postgres maximum declared precision and scale bounds remain exact -00:05 +32: test/decimal_precision_test.dart: decimal precision postgres historical backfills retain constrained decimal key declarations -00:05 +33: test/decimal_precision_test.dart: SQLite enforces stored precision and checks default coercion and trusted-schema behavior -00:05 +34: test/decimal_precision_test.dart: generation validates DecimalDigits and retains custom decimal codec types -00:07 +35: test/decimal_precision_test.dart: metadata rejects invalid precisions scales and non-decimal columns -00:07 +36: loading test/selection_test.dart -00:07 +36: test/selection_test.dart: selection sqlite all typed arities defer mapping and preserve left-to-right evaluation -00:07 +37: test/selection_test.dart: selection sqlite first decoder failure prevents later fields and outer mapper -00:07 +38: test/selection_test.dart: selection sqlite dynamic fields snapshot keys and preserve nulls and mapping order -00:07 +39: test/selection_test.dart: selection postgres all typed arities defer mapping and preserve left-to-right evaluation -00:07 +40: test/selection_test.dart: selection postgres first decoder failure prevents later fields and outer mapper -00:07 +41: test/selection_test.dart: selection postgres dynamic fields snapshot keys and preserve nulls and mapping order -00:07 +42: loading test/build_runner_test.dart -00:07 +42: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -00:31 +43: loading test/schema_version_test.dart -00:31 +43: test/schema_version_test.dart: schema version sqlite invalid ranges fail before SQL; caller transactions stay usable -00:31 +44: test/schema_version_test.dart: schema version sqlite unversioned existing data is rejected without creating history -00:31 +45: test/schema_version_test.dart: schema version sqlite default requires latest; inclusive compatibility range is explicit -00:31 +46: test/schema_version_test.dart: schema version sqlite newer database rejects downgrade without changing data or schema -00:31 +47: test/schema_version_test.dart: schema version sqlite complete applied prefix is checked, including old checksums and missing entries -00:31 +48: test/schema_version_test.dart: schema version sqlite matching version does not claim that the catalog is free of drift -00:31 +49: test/schema_version_test.dart: schema version sqlite verified baseline is compatible without replaying creation -00:31 +50: test/schema_version_test.dart: schema version sqlite borrowed session uses a read snapshot and survives rejected versions -00:31 +51: test/schema_version_test.dart: schema version postgres invalid ranges fail before SQL; caller transactions stay usable -00:31 +52: test/schema_version_test.dart: schema version postgres unversioned existing data is rejected without creating history -00:31 +53: test/schema_version_test.dart: schema version postgres default requires latest; inclusive compatibility range is explicit -00:31 +54: test/schema_version_test.dart: schema version postgres newer database rejects downgrade without changing data or schema -00:31 +55: test/schema_version_test.dart: schema version postgres complete applied prefix is checked, including old checksums and missing entries -00:32 +56: test/schema_version_test.dart: schema version postgres matching version does not claim that the catalog is free of drift -00:32 +57: test/schema_version_test.dart: schema version postgres verified baseline is compatible without replaying creation -00:32 +58: test/schema_version_test.dart: schema version postgres borrowed session uses a read snapshot and survives rejected versions -00:32 +59: test/schema_version_test.dart: schema version postgres unfinished recoverable migration blocks startup until repaired and completed -00:32 +60: test/schema_version_test.dart: schema version postgres catalog and history share a snapshot and the supplied history is frozen -00:32 +61: test/schema_version_test.dart: SQLite version gate works on an explicitly read-only file -00:32 +62: loading test/types_test.dart -00:32 +62: test/types_test.dart: junction-table keys, enum payloads and nested results keep generated types -00:33 +63: test/types_test.dart: computed generated fields are statically readable and never writable -00:34 +64: test/types_test.dart: client-default create parameters preserve domain, nullable and timestamp types -00:34 +65: test/types_test.dart: query-only relation APIs retain result types and expose no graph writes -00:35 +66: test/types_test.dart: calendar APIs reject instants, strings and mixed temporal types -00:35 +67: test/types_test.dart: actual generated APIs reject wrong inputs, results and backend options -00:36 +68: loading test/migration_test.dart -00:36 +68: test/migration_test.dart: (setUpAll) -00:36 +68: test/migration_test.dart: migrations sqlite explicit table and column renames preserve incoming references and data -00:36 +69: test/migration_test.dart: migrations sqlite constraint changes resolve catalog names and retain rows -00:36 +70: test/migration_test.dart: migrations sqlite failed NOT NULL rebuild restores data history and foreign key settings -00:36 +71: test/migration_test.dart: migrations sqlite reviewed conversion expressions preserve values when storage types change -00:36 +72: test/migration_test.dart: migrations sqlite additive changes use ALTER without a table rebuild -00:36 +73: test/migration_test.dart: migrations sqlite reviewed table removal handles restrictive foreign keys atomically -00:36 +74: test/migration_test.dart: migrations sqlite broken migration chain is refused before DDL -00:36 +75: test/migration_test.dart: migrations sqlite catalog distinguishes nullable non-rowid primary keys -00:36 +76: test/migration_test.dart: migrations sqlite rebuild preserves unmanaged index trigger and view definitions -00:36 +77: test/migration_test.dart: migrations sqlite foreign key check rolls back changed parent keys without cascading data away -00:36 +78: test/migration_test.dart: migrations sqlite unmodeled CHECK constraints are refused without losing their definition -00:36 +79: test/migration_test.dart: migrations postgres explicit table and column renames preserve incoming references and data -00:36 +80: test/migration_test.dart: migrations postgres constraint changes resolve catalog names and retain rows -00:36 +81: test/migration_test.dart: migrations postgres failed NOT NULL rebuild restores data history and foreign key settings -00:36 +82: test/migration_test.dart: migrations postgres reviewed conversion expressions preserve values when storage types change -00:36 +83: test/migration_test.dart: migrations postgres additive changes use ALTER without a table rebuild -00:36 +84: test/migration_test.dart: migrations postgres reviewed table removal handles restrictive foreign keys atomically -00:36 +85: test/migration_test.dart: migrations postgres broken migration chain is refused before DDL -00:36 +86: test/migration_test.dart: diff requires explicit renames, casts and backfills -00:36 +87: test/migration_test.dart: (tearDownAll) -00:36 +87: loading test/many_to_many_test.dart -00:36 +87: test/many_to_many_test.dart: many-to-many sqlite two declared FKs and a composite primary key enforce association identity -00:36 +88: test/many_to_many_test.dart: many-to-many sqlite forward projection keeps payload, shared targets and per-user limits in two statements -00:36 +89: test/many_to_many_test.dart: many-to-many sqlite reverse projection applies tie-breaking and offset to each team without changing root pagination -00:36 +90: test/many_to_many_test.dart: many-to-many sqlite nested team rosters deduplicate shared team keys and batch by level -00:36 +91: test/many_to_many_test.dart: many-to-many sqlite association predicates and counts stay in one SQL statement -00:36 +92: test/many_to_many_test.dart: many-to-many sqlite limited parameter capacity chunks associations without changing per-parent limits -00:36 +93: test/many_to_many_test.dart: many-to-many sqlite transaction rollback restores payload changes and new endpoints after an invalid association -00:36 +94: test/many_to_many_test.dart: many-to-many sqlite composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:36 +95: test/many_to_many_test.dart: many-to-many sqlite watch tracks junction payload and opposite endpoint changes after commit -00:36 +96: test/many_to_many_test.dart: many-to-many postgres two declared FKs and a composite primary key enforce association identity -00:37 +97: test/many_to_many_test.dart: many-to-many postgres forward projection keeps payload, shared targets and per-user limits in two statements -00:37 +98: test/many_to_many_test.dart: many-to-many postgres reverse projection applies tie-breaking and offset to each team without changing root pagination -00:37 +99: test/many_to_many_test.dart: many-to-many postgres nested team rosters deduplicate shared team keys and batch by level -00:37 +100: test/many_to_many_test.dart: many-to-many postgres association predicates and counts stay in one SQL statement -00:37 +101: test/many_to_many_test.dart: many-to-many postgres limited parameter capacity chunks associations without changing per-parent limits -00:37 +102: test/many_to_many_test.dart: many-to-many postgres transaction rollback restores payload changes and new endpoints after an invalid association -00:37 +103: test/many_to_many_test.dart: many-to-many postgres composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:37 +104: test/many_to_many_test.dart: many-to-many postgres watch tracks junction payload and opposite endpoint changes after commit -00:37 +105: loading test/stream_test.dart -00:37 +105: test/stream_test.dart: sqlite cancellation between batch chunks prevents a partial commit -00:37 +106: test/stream_test.dart: sqlite cancellation interrupts an active cursor fetch -00:37 +107: test/stream_test.dart: sqlite timed-out mutations return no partial write -00:37 +108: test/stream_test.dart: sqlite database close stops a paused cursor and releases the driver -00:37 +109: test/stream_test.dart: sqlite cancellation between stream creation and listening needs no rollback -00:37 +110: test/stream_test.dart: sqlite bounded cursor fetches preserve parameters and typed rows -00:37 +111: test/stream_test.dart: sqlite pause stops fetches and early cancellation releases the lease -00:37 +112: test/stream_test.dart: sqlite await-for provides demand and take closes without exhausting -00:37 +113: test/stream_test.dart: sqlite relation projections load once per root batch -00:37 +114: test/stream_test.dart: sqlite mapper failure closes the cursor and rolls back -00:37 +115: test/stream_test.dart: sqlite early stream end inside a transaction preserves other work -00:37 +116: test/stream_test.dart: sqlite escaping paused stream cannot keep a transaction alive -00:37 +117: test/stream_test.dart: sqlite cancels executing SQL and allows the next statement -00:37 +118: test/stream_test.dart: sqlite statement timeout stops SQL and leaves a usable connection -00:37 +119: test/stream_test.dart: sqlite cancelled statement poisons a transaction even if caught -00:37 +120: test/stream_test.dart: sqlite cancelling a paused stream releases its connection before resume -00:37 +121: test/stream_test.dart: sqlite cancellation before starting performs no SQL -00:37 +122: test/stream_test.dart: postgres cancellation between batch chunks prevents a partial commit -00:37 +123: test/stream_test.dart: postgres cancellation interrupts an active cursor fetch -00:37 +124: test/stream_test.dart: postgres timed-out mutations return no partial write -00:37 +125: test/stream_test.dart: postgres database close stops a paused cursor and releases the driver -00:37 +126: test/stream_test.dart: postgres cancellation between stream creation and listening needs no rollback -00:37 +127: test/stream_test.dart: postgres bounded cursor fetches preserve parameters and typed rows -00:37 +128: test/stream_test.dart: postgres pause stops fetches and early cancellation releases the lease -00:37 +129: test/stream_test.dart: postgres await-for provides demand and take closes without exhausting -00:38 +130: test/stream_test.dart: postgres relation projections load once per root batch -00:38 +131: test/stream_test.dart: postgres mapper failure closes the cursor and rolls back -00:38 +132: test/stream_test.dart: postgres early stream end inside a transaction preserves other work -00:38 +133: test/stream_test.dart: postgres escaping paused stream cannot keep a transaction alive -00:38 +134: test/stream_test.dart: postgres cancels executing SQL and allows the next statement -00:38 +135: test/stream_test.dart: postgres statement timeout stops SQL and leaves a usable connection -00:38 +136: test/stream_test.dart: postgres cancelled statement poisons a transaction even if caught -00:38 +137: test/stream_test.dart: postgres cancelling a paused stream releases its connection before resume -00:38 +138: test/stream_test.dart: postgres cancellation before starting performs no SQL -00:38 +139: test/stream_test.dart: late PostgreSQL cancellation cannot reach the next statement -00:38 +140: test/stream_test.dart: borrowed pool requires cancellation support and retains ownership -00:38 +141: test/stream_test.dart: failed cancellation control discards the physical connection -00:38 +142: loading test/migration_recovery_test.dart -00:38 +142: test/migration_recovery_test.dart: (setUpAll) -00:38 +142: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:38 +143: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:48 +144: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:48 +145: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:48 +146: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:48 +147: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:48 +148: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:48 +149: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:49 +150: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:49 +151: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:49 +152: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:49 +153: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -00:50 +154: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -00:51 +155: test/migration_recovery_test.dart: (tearDownAll) -00:51 +155: loading test/acquisition_test.dart -00:51 +155: test/acquisition_test.dart: an expired lease cannot enter before a delayed Timer gets its turn -00:51 +156: test/acquisition_test.dart: acquisition sqlite expired raw writes never execute after a late lease becomes available -00:51 +157: test/acquisition_test.dart: acquisition sqlite query cancellation returns while the pool is still occupied -00:51 +158: test/acquisition_test.dart: acquisition sqlite typed mutations and batch transactions propagate acquisition limits -00:51 +159: test/acquisition_test.dart: acquisition sqlite expired transactions and sessions never enter their callbacks -00:51 +160: test/acquisition_test.dart: acquisition sqlite acquisition timeout stops on entry and statement timeout starts afterward -00:51 +161: test/acquisition_test.dart: acquisition sqlite invalid and already-cancelled acquisition options never queue work -00:51 +162: test/acquisition_test.dart: acquisition sqlite stream cancellation and timeout return without opening a cursor -00:51 +163: test/acquisition_test.dart: acquisition sqlite query watch acquisition failure can recover on a later invalidation -00:51 +164: test/acquisition_test.dart: acquisition sqlite close drains abandoned reservations without running their callbacks -00:51 +165: test/acquisition_test.dart: acquisition sqlite abandoned requests preserve pool reuse and subsequent action errors -00:51 +166: test/acquisition_test.dart: PostgreSQL native pool timeout is classified before entering the callback -00:52 +167: test/acquisition_test.dart: PostgreSQL connection establishment timeout does not cap pool waiting -00:52 +168: test/acquisition_test.dart: acquisition postgres expired raw writes never execute after a late lease becomes available -00:52 +169: test/acquisition_test.dart: acquisition postgres query cancellation returns while the pool is still occupied -00:52 +170: test/acquisition_test.dart: acquisition postgres typed mutations and batch transactions propagate acquisition limits -00:52 +171: test/acquisition_test.dart: acquisition postgres expired transactions and sessions never enter their callbacks -00:52 +172: test/acquisition_test.dart: acquisition postgres acquisition timeout stops on entry and statement timeout starts afterward -00:52 +173: test/acquisition_test.dart: acquisition postgres invalid and already-cancelled acquisition options never queue work -00:52 +174: test/acquisition_test.dart: acquisition postgres stream cancellation and timeout return without opening a cursor -00:52 +175: test/acquisition_test.dart: acquisition postgres query watch acquisition failure can recover on a later invalidation -00:53 +176: test/acquisition_test.dart: acquisition postgres close drains abandoned reservations without running their callbacks -00:53 +177: test/acquisition_test.dart: acquisition postgres abandoned requests preserve pool reuse and subsequent action errors -00:53 +178: test/acquisition_test.dart: PostgreSQL timeout during real connection initialization releases the late lease -00:53 +179: loading test/computed_test.dart -00:53 +179: test/computed_test.dart: computed sqlite native expressions preserve quoted identifiers, comments and SQL-like string values -00:53 +180: test/computed_test.dart: computed sqlite generated CRUD, projections, batch and conflict writes compute stored and virtual values -00:53 +181: test/computed_test.dart: computed sqlite catalog/import roundtrip preserves SQL and modes and detects drift -00:55 +182: test/computed_test.dart: computed sqlite ComputedStorage.stored add, expression/type change and drop preserve existing rows -00:55 +183: test/computed_test.dart: computed sqlite ComputedStorage.virtual add, expression/type change and drop preserve existing rows -00:55 +184: test/computed_test.dart: computed sqlite stored to ordinary materialization retains values and permits later writes -00:55 +185: test/computed_test.dart: computed sqlite explicit renamed expressions preserve indexes, checks, references and views -00:55 +186: test/computed_test.dart: computed sqlite invalid recomputation rolls back the table and migration history -00:55 +187: test/computed_test.dart: computed sqlite decimal computed SQL keeps precision coercion and imports exactly once -00:56 +188: test/computed_test.dart: computed postgres native expressions preserve quoted identifiers, comments and SQL-like string values -00:56 +189: test/computed_test.dart: computed postgres generated CRUD, projections, batch and conflict writes compute stored and virtual values -00:56 +190: test/computed_test.dart: computed postgres catalog/import roundtrip preserves SQL and modes and detects drift -00:56 +191: test/computed_test.dart: computed postgres ComputedStorage.stored add, expression/type change and drop preserve existing rows -00:56 +192: test/computed_test.dart: computed postgres ComputedStorage.virtual add, expression/type change and drop preserve existing rows -00:56 +193: test/computed_test.dart: computed postgres stored to ordinary materialization retains values and permits later writes -00:56 +194: test/computed_test.dart: computed postgres explicit renamed expressions preserve indexes, checks, references and views -00:56 +195: test/computed_test.dart: computed postgres invalid recomputation rolls back the table and migration history -00:56 +196: test/computed_test.dart: computed postgres decimal computed SQL keeps precision coercion and imports exactly once -00:57 +197: test/computed_test.dart: mode conversions need explicit plans and historical copy/backfill cannot write computed columns -00:57 +198: loading test/named_sql_test.dart -00:57 +198: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -00:57 +199: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -00:57 +200: test/named_sql_test.dart: generated client and manifest are deterministic and fresh -00:59 +201: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -00:59 +202: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -00:59 +203: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -00:59 +204: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -00:59 +205: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -00:59 +206: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -01:00 +207: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -01:00 +208: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -01:00 +209: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -01:00 +210: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -01:00 +211: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -01:00 +212: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -01:00 +213: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -01:00 +214: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -01:00 +215: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -01:00 +216: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -01:00 +217: loading test/named_sql_generation_test.dart -01:00 +217: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -01:04 +218: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -01:05 +219: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -01:06 +220: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -01:07 +221: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -01:25 +222: loading test/decimal_average_test.dart -01:25 +222: test/decimal_average_test.dart: integer fractions round once and only bound their final result -01:26 +223: test/decimal_average_test.dart: decimal average sqlite empty null and constant inputs retain aggregate semantics -01:26 +224: test/decimal_average_test.dart: decimal average sqlite all rounding modes match exact signed sums across scales -01:26 +225: test/decimal_average_test.dart: decimal average sqlite valid means survive sums exceeding the finite numeric range -01:28 +226: test/decimal_average_test.dart: decimal average sqlite minimum fractions and outermost rounding scales stay exact -01:28 +227: test/decimal_average_test.dart: decimal average sqlite grouping HAVING ordering and CTE references preserve the mean -01:28 +228: test/decimal_average_test.dart: decimal average sqlite illegal window nesting and aggregate assignments fail before SQL -01:28 +229: test/decimal_average_test.dart: decimal average sqlite windows and rounded window means respect partitions and pagination -01:28 +230: test/decimal_average_test.dart: decimal average sqlite correlated means can update values and failures roll back transactions -01:28 +231: test/decimal_average_test.dart: decimal average sqlite batched relationship windows keep per-parent limits -01:28 +232: test/decimal_average_test.dart: decimal average sqlite sliding windows remove values, reset empty frames and reject varying policies -01:28 +233: test/decimal_average_test.dart: decimal average sqlite window means of grouped sums run after HAVING -01:28 +234: test/decimal_average_test.dart: decimal average postgres empty null and constant inputs retain aggregate semantics -01:28 +235: test/decimal_average_test.dart: decimal average postgres all rounding modes match exact signed sums across scales -01:28 +236: test/decimal_average_test.dart: decimal average postgres valid means survive sums exceeding the finite numeric range -01:29 +237: test/decimal_average_test.dart: decimal average postgres minimum fractions and outermost rounding scales stay exact -01:29 +238: test/decimal_average_test.dart: decimal average postgres grouping HAVING ordering and CTE references preserve the mean -01:29 +239: test/decimal_average_test.dart: decimal average postgres illegal window nesting and aggregate assignments fail before SQL -01:29 +240: test/decimal_average_test.dart: decimal average postgres windows and rounded window means respect partitions and pagination -01:29 +241: test/decimal_average_test.dart: decimal average postgres correlated means can update values and failures roll back transactions -01:29 +242: test/decimal_average_test.dart: decimal average postgres batched relationship windows keep per-parent limits -01:29 +243: test/decimal_average_test.dart: decimal average postgres window means of grouped sums run after HAVING -01:29 +244: test/decimal_average_test.dart: decimal average postgres volatile inputs and window ordering are evaluated once per row -01:29 +245: loading test/instant_test.dart -01:29 +245: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -01:29 +246: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -01:30 +247: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -01:30 +248: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -01:30 +249: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -01:30 +250: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -01:30 +251: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -01:32 +252: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -01:32 +253: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -01:32 +254: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -01:32 +255: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -01:32 +256: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -01:32 +257: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -01:32 +258: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -01:32 +259: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -01:32 +260: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -01:33 +261: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -01:33 +262: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -01:33 +263: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -01:33 +264: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -01:33 +265: loading test/codec_test.dart -01:33 +265: test/codec_test.dart: numeric aggregate text decodes without integer rounding or overflow -01:33 +266: test/codec_test.dart: erased codecs keep checked argument types without function getter casts -01:33 +267: test/codec_test.dart: mapped domain codecs preserve one decode and one encode boundary -01:33 +268: test/codec_test.dart: parsed JSON scalars and document presence have unambiguous decoding -01:33 +269: test/codec_test.dart: enum mappings reject duplicates and do not follow mutable caller maps -01:33 +270: loading test/sql_real_test.dart -01:33 +270: test/sql_real_test.dart: sqlite explicitly typed REAL parameters preserve floating storage -01:33 +271: test/sql_real_test.dart: postgres explicitly typed REAL parameters preserve floating storage -01:33 +272: loading test/generator_test.dart -01:33 +272: test/generator_test.dart: (setUpAll) -01:33 +272: test/generator_test.dart: record annotations, physical names, relations and snapshots -01:35 +273: test/generator_test.dart: output is deterministic and matches the committed generated client -01:40 +274: test/generator_test.dart: composite keys and more than six fields generate analyzable code -01:40 +275: test/generator_test.dart: computed keys are rejected instead of executing a fake row -01:41 +276: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -01:43 +277: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -01:45 +278: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty overrides and duplicate names -01:47 +279: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -01:47 +280: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -01:48 +281: test/generator_test.dart: invalid custom declaration: wrong_codec -01:48 +282: test/generator_test.dart: invalid custom declaration: nullable_codec -01:48 +283: test/generator_test.dart: invalid custom declaration: erased_extension -01:48 +284: test/generator_test.dart: invalid custom declaration: unknown_storage -01:49 +285: test/generator_test.dart: invalid custom declaration: private_codec -01:49 +286: test/generator_test.dart: invalid custom declaration: private_type -01:49 +287: test/generator_test.dart: invalid custom declaration: inline_codec -01:49 +288: test/generator_test.dart: invalid custom declaration: duplicate_codec -01:50 +289: test/generator_test.dart: invalid custom declaration: missing_codec -01:50 +290: test/generator_test.dart: invalid custom declaration: duplicate_labels -01:50 +291: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -01:50 +292: test/generator_test.dart: foreign keys require unique targets -01:51 +293: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -01:51 +294: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -01:51 +295: test/generator_test.dart: invalid query-only relation: lookup_computed -01:51 +296: test/generator_test.dart: invalid query-only relation: lookup_codec -01:52 +297: test/generator_test.dart: invalid query-only relation: lookup_inverse -01:52 +298: test/generator_test.dart: invalid query-only relation: lookup_type -01:52 +299: test/generator_test.dart: invalid query-only relation: lookup_delete -01:52 +300: test/generator_test.dart: Dart type errors stop generation -01:53 +301: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -01:53 +302: test/generator_test.dart: (tearDownAll) -01:53 +302: loading test/transaction_test.dart -01:53 +302: test/transaction_test.dart: transaction control sqlite idle callback expires, rolls back and cannot issue late SQL -01:53 +303: test/transaction_test.dart: transaction control sqlite deadline interrupts active SQL before releasing and reusing the lease -01:53 +304: test/transaction_test.dart: transaction control sqlite interrupted write rolls back and leaves the connection usable -01:53 +305: test/transaction_test.dart: transaction control sqlite explicit transaction cancellation stops an idle callback and active SQL -01:54 +306: test/transaction_test.dart: transaction control sqlite global transaction cancellation also abandons acquisition -01:54 +307: test/transaction_test.dart: transaction control sqlite savepoint inherits deadline, drains and rolls back through raw cleanup -01:54 +308: test/transaction_test.dart: transaction control sqlite deadline closes a paused transactional cursor before rollback -01:54 +309: test/transaction_test.dart: transaction control sqlite deadline interrupts an active transactional cursor fetch -01:54 +310: test/transaction_test.dart: transaction control sqlite transaction clock starts after acquisition and a borrowed session recovers -01:54 +311: test/transaction_test.dart: transaction control sqlite failed rollback is explicit and discards an uncertain connection -01:54 +312: test/transaction_test.dart: transaction control sqlite transaction controls reject adapters without actual cancellation -01:54 +313: test/transaction_test.dart: transaction control sqlite shorter statement limit retains its error and rolls back the transaction -01:54 +314: test/transaction_test.dart: transaction control sqlite monotonic deadline prevents commit after a CPU-bound callback -01:54 +315: test/transaction_test.dart: transaction control sqlite completed scopes remove their timers and cancellation listeners -01:54 +316: test/transaction_test.dart: transaction control sqlite confirmed commit wins a cancellation race; lost acknowledgement stays unknown -01:54 +317: test/transaction_test.dart: transaction control sqlite deferred constraint rejection at COMMIT is a known failure -01:54 +318: test/transaction_test.dart: transaction control sqlite invalid deadlines and already cancelled tokens never begin a transaction -01:54 +319: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=false -01:54 +320: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=true -01:54 +321: test/transaction_test.dart: transaction control postgres idle callback expires, rolls back and cannot issue late SQL -01:55 +322: test/transaction_test.dart: transaction control postgres deadline interrupts active SQL before releasing and reusing the lease -01:55 +323: test/transaction_test.dart: transaction control postgres interrupted write rolls back and leaves the connection usable -01:55 +324: test/transaction_test.dart: transaction control postgres explicit transaction cancellation stops an idle callback and active SQL -01:55 +325: test/transaction_test.dart: transaction control postgres global transaction cancellation also abandons acquisition -01:55 +326: test/transaction_test.dart: transaction control postgres savepoint inherits deadline, drains and rolls back through raw cleanup -01:55 +327: test/transaction_test.dart: transaction control postgres deadline closes a paused transactional cursor before rollback -01:55 +328: test/transaction_test.dart: transaction control postgres deadline interrupts an active transactional cursor fetch -01:55 +329: test/transaction_test.dart: transaction control postgres transaction clock starts after acquisition and a borrowed session recovers -01:56 +330: test/transaction_test.dart: transaction control postgres failed rollback is explicit and discards an uncertain connection -01:56 +331: test/transaction_test.dart: transaction control postgres transaction controls reject adapters without actual cancellation -01:56 +332: test/transaction_test.dart: transaction control postgres shorter statement limit retains its error and rolls back the transaction -01:56 +333: test/transaction_test.dart: transaction control postgres monotonic deadline prevents commit after a CPU-bound callback -01:56 +334: test/transaction_test.dart: transaction control postgres completed scopes remove their timers and cancellation listeners -01:56 +335: test/transaction_test.dart: transaction control postgres confirmed commit wins a cancellation race; lost acknowledgement stays unknown -01:56 +336: test/transaction_test.dart: transaction control postgres deferred constraint rejection at COMMIT is a known failure -01:56 +337: test/transaction_test.dart: transaction control postgres invalid deadlines and already cancelled tokens never begin a transaction -01:56 +338: test/transaction_test.dart: transaction control postgres SQLSTATE 40003 at COMMIT is never classified as a confirmed rejection -01:56 +339: test/transaction_test.dart: transaction control postgres real PostgreSQL serialization failure is classified after rollback -01:56 +340: test/transaction_test.dart: transaction control postgres real PostgreSQL deadlock rolls back exactly one transaction -01:57 +341: test/transaction_test.dart: SQLite COMMIT busy is a known rejection followed by verified rollback -01:57 +342: test/transaction_test.dart: SQLite deadline while BEGIN waits for a writer preserves its connection -01:57 +343: loading test/backfill_keys_test.dart -01:57 +343: test/backfill_keys_test.dart: backfill keys sqlite integer cursors preserve native values across resume -01:58 +344: test/backfill_keys_test.dart: backfill keys sqlite bigint cursors preserve native values across resume -01:58 +345: test/backfill_keys_test.dart: backfill keys sqlite text cursors preserve native values across resume -01:58 +346: test/backfill_keys_test.dart: backfill keys sqlite real cursors preserve native values across resume -01:58 +347: test/backfill_keys_test.dart: backfill keys sqlite boolean cursors preserve native values across resume -01:58 +348: test/backfill_keys_test.dart: backfill keys sqlite timestamp cursors preserve native values across resume -01:58 +349: test/backfill_keys_test.dart: backfill keys sqlite blob cursors preserve native values across resume -01:58 +350: test/backfill_keys_test.dart: backfill keys sqlite composite key batches account for every bound parameter -01:58 +351: test/backfill_keys_test.dart: backfill keys postgres integer cursors preserve native values across resume -01:58 +352: test/backfill_keys_test.dart: backfill keys postgres bigint cursors preserve native values across resume -01:58 +353: test/backfill_keys_test.dart: backfill keys postgres text cursors preserve native values across resume -01:58 +354: test/backfill_keys_test.dart: backfill keys postgres real cursors preserve native values across resume -01:58 +355: test/backfill_keys_test.dart: backfill keys postgres boolean cursors preserve native values across resume -01:58 +356: test/backfill_keys_test.dart: backfill keys postgres timestamp cursors preserve native values across resume -01:58 +357: test/backfill_keys_test.dart: backfill keys postgres blob cursors preserve native values across resume -01:58 +358: test/backfill_keys_test.dart: backfill keys postgres composite key batches account for every bound parameter -01:58 +359: test/backfill_keys_test.dart: invalid key, assignments and completion declarations fail before execution -01:58 +360: loading test/retry_test.dart -01:58 +360: test/retry_test.dart: retry sqlite opt-in retries roll back all writes and use new closed-over views -01:58 +361: test/retry_test.dart: retry sqlite default is one attempt; an explicit attempt limit is exact -01:58 +362: test/retry_test.dart: retry sqlite application and constraint errors and caught statement failures do not replay -01:58 +363: test/retry_test.dart: retry sqlite unknown commit and unconfirmed rollback prohibit replay -01:58 +364: test/retry_test.dart: retry sqlite retry time budget bounds active SQL and does not retry cancellation -01:58 +365: test/retry_test.dart: retry sqlite deadline and cancellation interrupt backoff without another callback -01:58 +366: test/retry_test.dart: retry sqlite time budget includes acquisition and abandons late callbacks -01:59 +367: test/retry_test.dart: retry sqlite transaction timeout is shared across attempts and sessions recover after expiry -01:59 +368: test/retry_test.dart: retry sqlite subscriptions publish only the final committed retry result -01:59 +369: test/retry_test.dart: retry sqlite invalid policies reject before SQL -01:59 +370: test/retry_test.dart: retry postgres opt-in retries roll back all writes and use new closed-over views -01:59 +371: test/retry_test.dart: retry postgres default is one attempt; an explicit attempt limit is exact -01:59 +372: test/retry_test.dart: retry postgres application and constraint errors and caught statement failures do not replay -01:59 +373: test/retry_test.dart: retry postgres unknown commit and unconfirmed rollback prohibit replay -01:59 +374: test/retry_test.dart: retry postgres retry time budget bounds active SQL and does not retry cancellation -01:59 +375: test/retry_test.dart: retry postgres deadline and cancellation interrupt backoff without another callback -01:59 +376: test/retry_test.dart: retry postgres time budget includes acquisition and abandons late callbacks -01:59 +377: test/retry_test.dart: retry postgres transaction timeout is shared across attempts and sessions recover after expiry -01:59 +378: test/retry_test.dart: retry postgres subscriptions publish only the final committed retry result -01:59 +379: test/retry_test.dart: retry postgres invalid policies reject before SQL -01:59 +380: test/retry_test.dart: retry postgres real unknown COMMIT response prohibits opted-in retry -01:59 +381: test/retry_test.dart: retry postgres real serialization conflict repeats the reads and decisions -01:59 +382: test/retry_test.dart: retry postgres real deadlock retries only the aborted callback -02:00 +383: test/retry_test.dart: SQLite WAL snapshot conflict replays reads after rollback -02:00 +384: test/retry_test.dart: SQLite real busy COMMIT: success -02:00 +385: test/retry_test.dart: SQLite real busy COMMIT: exhausted -02:01 +386: test/retry_test.dart: SQLite real busy COMMIT: deadline -02:01 +387: test/retry_test.dart: SQLite real busy COMMIT: cancel -02:01 +388: test/retry_test.dart: SQLite real busy COMMIT: shared-budget -02:01 +389: loading test/builder_test.dart -02:01 +389: test/builder_test.dart: build assets and standalone CLI produce identical client and snapshot -02:04 +390: test/builder_test.dart: multiple explicit schema roots resolve shared public types under lib -02:04 +391: test/builder_test.dart: invalid semantic_error never publishes partial outputs -02:05 +392: test/builder_test.dart: invalid enum_labels never publishes partial outputs -02:06 +393: test/builder_test.dart: invalid no_models never publishes partial outputs -02:06 +394: test/builder_test.dart: invalid part_file never publishes partial outputs -02:07 +395: test/builder_test.dart: unknown options fail instead of silently changing build behavior -02:07 +396: test/builder_test.dart: schema resolution can consume an earlier builder output -02:08 +397: loading test/temporal_precision_test.dart -02:08 +397: test/temporal_precision_test.dart: temporal precision sqlite generated writes, defaults and computed columns use database precision -02:08 +398: test/temporal_precision_test.dart: temporal precision sqlite SQL casts match all precisions on both sides of the PostgreSQL epoch -02:08 +399: test/temporal_precision_test.dart: temporal precision sqlite nulls, CTE references, grouping, streams and wrong scopes retain type semantics -02:08 +400: test/temporal_precision_test.dart: temporal precision sqlite rounded keys work in batch writes, upserts and foreign-key relation loading -02:08 +401: test/temporal_precision_test.dart: temporal precision sqlite catalog import preserves precision, defaults and computed expressions -02:10 +402: test/temporal_precision_test.dart: temporal precision sqlite narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -02:10 +403: test/temporal_precision_test.dart: temporal precision sqlite timestamp defaults and explicit six-digit catalogs retain precision -02:10 +404: test/temporal_precision_test.dart: temporal precision sqlite extended timestamps round in SQL without total-microsecond overflow -02:10 +405: test/temporal_precision_test.dart: temporal precision sqlite raw SQL respects native or managed precision constraints -02:10 +406: test/temporal_precision_test.dart: temporal precision postgres generated writes, defaults and computed columns use database precision -02:10 +407: test/temporal_precision_test.dart: temporal precision postgres SQL casts match all precisions on both sides of the PostgreSQL epoch -02:10 +408: test/temporal_precision_test.dart: temporal precision postgres nulls, CTE references, grouping, streams and wrong scopes retain type semantics -02:10 +409: test/temporal_precision_test.dart: temporal precision postgres rounded keys work in batch writes, upserts and foreign-key relation loading -02:10 +410: test/temporal_precision_test.dart: temporal precision postgres catalog import preserves precision, defaults and computed expressions -02:11 +411: test/temporal_precision_test.dart: temporal precision postgres narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -02:11 +412: test/temporal_precision_test.dart: temporal precision postgres timestamp defaults and explicit six-digit catalogs retain precision -02:11 +413: test/temporal_precision_test.dart: temporal precision postgres extended timestamps round in SQL without total-microsecond overflow -02:11 +414: test/temporal_precision_test.dart: temporal precision postgres raw SQL respects native or managed precision constraints -02:11 +415: test/temporal_precision_test.dart: value rounding keeps extended ranges and rejects overflow/invalid precision -02:11 +416: test/temporal_precision_test.dart: default and explicit six preserve historical snapshots and migration checksums -02:11 +417: test/temporal_precision_test.dart: generator rejects invalid annotations and preserves generated fixture metadata -02:13 +418: loading test/observation_test.dart -02:13 +418: test/observation_test.dart: observation sqlite actual lease queue and synchronous mapping have separate clocks -02:13 +419: test/observation_test.dart: observation sqlite expired acquisition emits one failure and no late SQL -02:13 +420: test/observation_test.dart: observation sqlite cancelled acquisition emits one failure and no late SQL -02:13 +421: test/observation_test.dart: observation sqlite sessions transactions and savepoints inherit hooks and report reused leases -02:13 +422: test/observation_test.dart: observation sqlite nested reads attribute decoding to each statement without repeating SQL -02:13 +423: test/observation_test.dart: observation sqlite SQL and mapper failures belong to their own phases -02:13 +424: test/observation_test.dart: observation sqlite cursor batches and returning writes produce decoding observations -02:13 +425: test/observation_test.dart: observation sqlite observer exceptions preserve successful commits and original mapper errors -02:13 +426: test/observation_test.dart: observation postgres actual lease queue and synchronous mapping have separate clocks -02:14 +427: test/observation_test.dart: observation postgres expired acquisition emits one failure and no late SQL -02:14 +428: test/observation_test.dart: observation postgres cancelled acquisition emits one failure and no late SQL -02:14 +429: test/observation_test.dart: observation postgres sessions transactions and savepoints inherit hooks and report reused leases -02:14 +430: test/observation_test.dart: observation postgres nested reads attribute decoding to each statement without repeating SQL -02:14 +431: test/observation_test.dart: observation postgres SQL and mapper failures belong to their own phases -02:14 +432: test/observation_test.dart: observation postgres cursor batches and returning writes produce decoding observations -02:14 +433: test/observation_test.dart: observation postgres observer exceptions preserve successful commits and original mapper errors -02:14 +434: test/observation_test.dart: synchronous driver acquisition failures are reported once -02:14 +435: loading test/integer_test.dart -02:14 +435: test/integer_test.dart: integer widths sqlite storage bounds, nulls and wider sums keep the normal integer value codec -02:14 +436: test/integer_test.dart: integer widths sqlite relations, union and cursors share int expressions across physical widths -02:14 +437: test/integer_test.dart: integer widths sqlite defaulted and quoted columns retain enforced nullable bounds -02:14 +438: test/integer_test.dart: integer widths sqlite JSON history and imports retain widths without confusing value codecs -02:16 +439: test/integer_test.dart: integer widths sqlite widening and narrowing use reviewed conversions with atomic failure recovery -02:16 +440: test/integer_test.dart: integer widths sqlite column renames preserve width metadata and range constraints -02:16 +441: test/integer_test.dart: integer widths sqlite historical bounded backfills verify widths and resume normally -02:16 +442: test/integer_test.dart: integer widths sqlite changing an identity width preserves generation and rejects out-of-range explicit IDs -02:16 +443: test/integer_test.dart: integer widths sqlite quoted defaults and comments cannot masquerade as managed range checks -02:16 +444: test/integer_test.dart: integer widths sqlite extra weaker expressions remain unmanaged and cannot claim a narrower width -02:16 +445: test/integer_test.dart: integer widths postgres storage bounds, nulls and wider sums keep the normal integer value codec -02:16 +446: test/integer_test.dart: integer widths postgres relations, union and cursors share int expressions across physical widths -02:16 +447: test/integer_test.dart: integer widths postgres defaulted and quoted columns retain enforced nullable bounds -02:16 +448: test/integer_test.dart: integer widths postgres JSON history and imports retain widths without confusing value codecs -02:17 +449: test/integer_test.dart: integer widths postgres widening and narrowing use reviewed conversions with atomic failure recovery -02:17 +450: test/integer_test.dart: integer widths postgres column renames preserve width metadata and range constraints -02:17 +451: test/integer_test.dart: integer widths postgres historical bounded backfills verify widths and resume normally -02:17 +452: test/integer_test.dart: integer widths postgres changing an identity width preserves generation and rejects out-of-range explicit IDs -02:17 +453: test/integer_test.dart: analyzer validates width annotations and integer-backed domain codecs -02:18 +454: test/integer_test.dart: width declarations reject wrong kinds and ranges without changing default snapshots -02:18 +455: loading test/relation_test.dart -02:18 +455: test/relation_test.dart: sqlite automatic unique joins preserve root pagination in one statement -02:18 +456: test/relation_test.dart: sqlite all-null projections still distinguish a present row from absence -02:18 +457: test/relation_test.dart: sqlite explicit batch deduplicates composite keys and skips partial nulls -02:18 +458: test/relation_test.dart: sqlite composite chunks include filter and window parameters with nested joins -02:18 +459: test/relation_test.dart: sqlite nested self joins and two edges to the same table use distinct aliases -02:18 +460: test/relation_test.dart: sqlite batch collection below a joined optional parent loads on the same plan -02:18 +461: test/relation_test.dart: sqlite join refuses unproven cardinality while automatic batch checks it -02:18 +462: test/relation_test.dart: sqlite filters and to-one pagination do not drop root rows -02:19 +463: test/relation_test.dart: sqlite joined records survive CTE projection and cursor decoding -02:19 +464: test/relation_test.dart: sqlite required tests row presence independently of nullable selected values -02:19 +465: test/relation_test.dart: sqlite relation aggregates restore the alias used by a joined selection -02:19 +466: test/relation_test.dart: sqlite reusing an identical edge deduplicates its join -02:19 +467: test/relation_test.dart: sqlite different filters need fresh relation occurrences -02:19 +468: test/relation_test.dart: sqlite large composite batches avoid expression-depth failure -02:19 +469: test/relation_test.dart: postgres automatic unique joins preserve root pagination in one statement -02:19 +470: test/relation_test.dart: postgres all-null projections still distinguish a present row from absence -02:19 +471: test/relation_test.dart: postgres explicit batch deduplicates composite keys and skips partial nulls -02:19 +472: test/relation_test.dart: postgres composite chunks include filter and window parameters with nested joins -02:19 +473: test/relation_test.dart: postgres nested self joins and two edges to the same table use distinct aliases -02:19 +474: test/relation_test.dart: postgres batch collection below a joined optional parent loads on the same plan -02:19 +475: test/relation_test.dart: postgres join refuses unproven cardinality while automatic batch checks it -02:19 +476: test/relation_test.dart: postgres filters and to-one pagination do not drop root rows -02:19 +477: test/relation_test.dart: postgres joined records survive CTE projection and cursor decoding -02:19 +478: test/relation_test.dart: postgres required tests row presence independently of nullable selected values -02:19 +479: test/relation_test.dart: postgres relation aggregates restore the alias used by a joined selection -02:19 +480: test/relation_test.dart: postgres reusing an identical edge deduplicates its join -02:19 +481: test/relation_test.dart: postgres different filters need fresh relation occurrences -02:19 +482: test/relation_test.dart: postgres large composite batches avoid expression-depth failure -02:19 +483: loading test/generated_database_test.dart -02:19 +483: test/generated_database_test.dart: (setUpAll) -02:19 +483: test/generated_database_test.dart: generated sqlite generated client, DDL, relations, dates and patches work together -02:19 +484: test/generated_database_test.dart: generated sqlite planning is read-only and repeat migration is a no-op -02:19 +485: test/generated_database_test.dart: generated sqlite snapshot round trip and full managed catalog comparison -02:19 +486: test/generated_database_test.dart: generated sqlite baseline preserves existing rows and verifies keys before recording history -02:19 +487: test/generated_database_test.dart: generated sqlite full verification sees defaults and additional unique constraints -02:19 +488: test/generated_database_test.dart: generated sqlite history tampering and missing applied migrations fail -02:19 +489: test/generated_database_test.dart: generated sqlite failed DDL rolls back schema and history atomically -02:19 +490: test/generated_database_test.dart: generated sqlite actual column drift is detected independently of history -02:19 +491: test/generated_database_test.dart: generated sqlite transaction control cannot be hidden behind SQL comments -02:19 +492: test/generated_database_test.dart: generated sqlite concurrent migration callers serialize against shared history -02:19 +493: test/generated_database_test.dart: generated postgres generated client, DDL, relations, dates and patches work together -02:19 +494: test/generated_database_test.dart: generated postgres planning is read-only and repeat migration is a no-op -02:19 +495: test/generated_database_test.dart: generated postgres snapshot round trip and full managed catalog comparison -02:19 +496: test/generated_database_test.dart: generated postgres baseline preserves existing rows and verifies keys before recording history -02:19 +497: test/generated_database_test.dart: generated postgres full verification sees defaults and additional unique constraints -02:19 +498: test/generated_database_test.dart: generated postgres history tampering and missing applied migrations fail -02:19 +499: test/generated_database_test.dart: generated postgres failed DDL rolls back schema and history atomically -02:19 +500: test/generated_database_test.dart: generated postgres actual column drift is detected independently of history -02:19 +501: test/generated_database_test.dart: generated postgres transaction control cannot be hidden behind SQL comments -02:19 +502: test/generated_database_test.dart: generated postgres concurrent migration callers serialize against shared history -02:19 +503: test/generated_database_test.dart: (tearDownAll) -02:19 +503: loading test/import_test.dart -02:20 +503: test/import_test.dart: catalog import sqlite imports records, identities, defaults and constraints then baselines unchanged rows -02:25 +504: test/import_test.dart: catalog import sqlite normalizes reserved, colliding and hostile identifiers without changing physical names -02:26 +505: test/import_test.dart: catalog import sqlite composite and self relations preserve key order and unique indexes -02:27 +506: test/import_test.dart: catalog import sqlite unsupported storage excludes its table while computed SQL imports with a review note -02:27 +507: test/import_test.dart: catalog import sqlite read-only snapshot and selected scope reject temporary shadowing and active transactions -02:27 +508: test/import_test.dart: catalog import sqlite physical storage mapping does not infer domain types from data -02:28 +509: test/import_test.dart: catalog import sqlite views triggers and specialized indexes remain in the review report -02:28 +510: test/import_test.dart: catalog import sqlite nullable primary keys, virtual tables and their storage tables need explicit handling -02:28 +511: test/import_test.dart: catalog import sqlite foreign keys with coercible but different Dart types are reported -02:28 +512: test/import_test.dart: catalog import postgres imports records, identities, defaults and constraints then baselines unchanged rows -02:30 +513: test/import_test.dart: catalog import postgres normalizes reserved, colliding and hostile identifiers without changing physical names -02:30 +514: test/import_test.dart: catalog import postgres composite and self relations preserve key order and unique indexes -02:31 +515: test/import_test.dart: catalog import postgres unsupported storage excludes its table while computed SQL imports with a review note -02:31 +516: test/import_test.dart: catalog import postgres read-only snapshot and selected scope reject temporary shadowing and active transactions -02:31 +517: test/import_test.dart: catalog import postgres physical storage mapping does not infer domain types from data -02:32 +518: test/import_test.dart: catalog import postgres views triggers and specialized indexes remain in the review report -02:32 +519: test/import_test.dart: catalog import postgres policy reports retain roles, commands, mode and row-security state -02:32 +520: test/import_test.dart: catalog import postgres always identity and inherited tables are not flattened into ordinary writable models -02:32 +521: loading test/database_test.dart -02:32 +521: test/database_test.dart: sqlite typed CRUD, exact projection and parameter binding -02:32 +522: test/database_test.dart: sqlite record and DTO mapping run only when decoding -02:32 +523: test/database_test.dart: sqlite immutable filters, empty IN, null and pagination -02:32 +524: test/database_test.dart: sqlite cross-table identity and duplicate assignments rejected before IO -02:32 +525: test/database_test.dart: sqlite commit, rollback, savepoint recovery and escaped session -02:33 +526: test/database_test.dart: sqlite caught statement failures cannot masquerade as a commit -02:33 +527: test/database_test.dart: sqlite recoverable constraint errors stay within their savepoint -02:33 +528: test/database_test.dart: sqlite unawaited work is drained and rolled back -02:33 +529: test/database_test.dart: sqlite close waits for active operations and rejects further use -02:33 +530: test/database_test.dart: sqlite leased sessions retain one connection across transactions and expire -02:33 +531: test/database_test.dart: sqlite native upsert updates atomically using existing and incoming values -02:33 +532: test/database_test.dart: sqlite bulk inserts split at parameter limits and opt into returning -02:33 +533: test/database_test.dart: sqlite a failed later bulk chunk rolls back all previous chunks -02:33 +534: test/database_test.dart: sqlite bulk optional field shapes preserve defaults and explicit null -02:33 +535: test/database_test.dart: sqlite relations CTEs preserve SQL column identity through mapped projections -02:33 +536: test/database_test.dart: sqlite relations explicit aliases support multiple joins and self joins -02:33 +537: test/database_test.dart: sqlite relations left join distinguishes a missing row from an all-null selection -02:33 +538: test/database_test.dart: sqlite relations correlated scalar, IN and EXISTS use nested SQL scopes -02:33 +539: test/database_test.dart: sqlite relations grouping is checked and window functions preserve row cardinality -02:33 +540: test/database_test.dart: sqlite relations root pagination and each parent limit use two queries -02:33 +541: test/database_test.dart: sqlite relations empty roots skip child queries; empty lists remain typed lists -02:33 +542: test/database_test.dart: sqlite relations nested relation projections and optional all-null projection -02:33 +543: test/database_test.dart: sqlite relations correlated any, none, count and every with SQL nulls -02:33 +544: test/database_test.dart: sqlite relations relation parameters bind correctly in projection and filter -02:33 +545: test/database_test.dart: sqlite keyset pagination uses a unique tie breaker in mixed directions -02:33 +546: test/database_test.dart: sqlite keyset pagination handles null order and validates versioned tokens -02:33 +547: test/database_test.dart: postgres typed CRUD, exact projection and parameter binding -02:33 +548: test/database_test.dart: postgres record and DTO mapping run only when decoding -02:33 +549: test/database_test.dart: postgres immutable filters, empty IN, null and pagination -02:33 +550: test/database_test.dart: postgres cross-table identity and duplicate assignments rejected before IO -02:33 +551: test/database_test.dart: postgres commit, rollback, savepoint recovery and escaped session -02:33 +552: test/database_test.dart: postgres caught statement failures cannot masquerade as a commit -02:33 +553: test/database_test.dart: postgres recoverable constraint errors stay within their savepoint -02:33 +554: test/database_test.dart: postgres unawaited work is drained and rolled back -02:33 +555: test/database_test.dart: postgres close waits for active operations and rejects further use -02:33 +556: test/database_test.dart: postgres leased sessions retain one connection across transactions and expire -02:33 +557: test/database_test.dart: postgres native upsert updates atomically using existing and incoming values -02:33 +558: test/database_test.dart: postgres bulk inserts split at parameter limits and opt into returning -02:33 +559: test/database_test.dart: postgres a failed later bulk chunk rolls back all previous chunks -02:33 +560: test/database_test.dart: postgres bulk optional field shapes preserve defaults and explicit null -02:33 +561: test/database_test.dart: postgres relations CTEs preserve SQL column identity through mapped projections -02:33 +562: test/database_test.dart: postgres relations explicit aliases support multiple joins and self joins -02:33 +563: test/database_test.dart: postgres relations left join distinguishes a missing row from an all-null selection -02:33 +564: test/database_test.dart: postgres relations correlated scalar, IN and EXISTS use nested SQL scopes -02:33 +565: test/database_test.dart: postgres relations grouping is checked and window functions preserve row cardinality -02:33 +566: test/database_test.dart: postgres relations root pagination and each parent limit use two queries -02:33 +567: test/database_test.dart: postgres relations empty roots skip child queries; empty lists remain typed lists -02:33 +568: test/database_test.dart: postgres relations nested relation projections and optional all-null projection -02:33 +569: test/database_test.dart: postgres relations correlated any, none, count and every with SQL nulls -02:33 +570: test/database_test.dart: postgres relations relation parameters bind correctly in projection and filter -02:33 +571: test/database_test.dart: postgres keyset pagination uses a unique tie breaker in mixed directions -02:33 +572: test/database_test.dart: postgres keyset pagination handles null order and validates versioned tokens -02:33 +573: loading test/union_test.dart -02:33 +573: test/union_test.dart: union sqlite SQL duplicate and NULL semantics use one statement -02:33 +574: test/union_test.dart: union sqlite heterogeneous records map only after SQL union and final pagination -02:33 +575: test/union_test.dart: union sqlite operand limits, order, parameters and final filter stay scoped -02:33 +576: test/union_test.dart: union sqlite repeated SQL expressions preserve positional columns -02:33 +577: test/union_test.dart: union sqlite nested sets preserve association, CTE exports and streaming -02:33 +578: test/union_test.dart: union sqlite same CTE names in distinct operands stay in separate scopes -02:33 +579: test/union_test.dart: union sqlite SQL scalar, IN subqueries, aggregate and zero-row results compose -02:33 +580: test/union_test.dart: union sqlite grouped branches and window branches remain SQL queries -02:33 +581: test/union_test.dart: union sqlite bound constant projections retain Dart types -02:33 +582: test/union_test.dart: union sqlite Dart maps and relation selections are rejected before execution -02:33 +583: test/union_test.dart: union sqlite codecs and nullability cannot silently change across operands -02:33 +584: test/union_test.dart: union sqlite shared domain codecs work, and nullable wrappers retain decoding semantics -02:33 +585: test/union_test.dart: union sqlite export references and outer join nullability are guarded -02:33 +586: test/union_test.dart: union sqlite compound mutations and mixed sessions fail before SQL -02:33 +587: test/union_test.dart: union sqlite all record arities use typed SQL decoding -02:33 +588: test/union_test.dart: union sqlite subscription observes committed changes in both operands -02:33 +589: test/union_test.dart: union postgres SQL duplicate and NULL semantics use one statement -02:33 +590: test/union_test.dart: union postgres heterogeneous records map only after SQL union and final pagination -02:33 +591: test/union_test.dart: union postgres operand limits, order, parameters and final filter stay scoped -02:33 +592: test/union_test.dart: union postgres repeated SQL expressions preserve positional columns -02:33 +593: test/union_test.dart: union postgres nested sets preserve association, CTE exports and streaming -02:33 +594: test/union_test.dart: union postgres same CTE names in distinct operands stay in separate scopes -02:33 +595: test/union_test.dart: union postgres SQL scalar, IN subqueries, aggregate and zero-row results compose -02:33 +596: test/union_test.dart: union postgres grouped branches and window branches remain SQL queries -02:33 +597: test/union_test.dart: union postgres bound constant projections retain Dart types -02:33 +598: test/union_test.dart: union postgres Dart maps and relation selections are rejected before execution -02:33 +599: test/union_test.dart: union postgres codecs and nullability cannot silently change across operands -02:33 +600: test/union_test.dart: union postgres shared domain codecs work, and nullable wrappers retain decoding semantics -02:33 +601: test/union_test.dart: union postgres export references and outer join nullability are guarded -02:33 +602: test/union_test.dart: union postgres compound mutations and mixed sessions fail before SQL -02:33 +603: test/union_test.dart: union postgres all record arities use typed SQL decoding -02:33 +604: test/union_test.dart: union postgres subscription observes committed changes in both operands -02:33 +605: loading test/import_cli_test.dart -02:34 +605: test/import_cli_test.dart: CLI sqlite imports reviews generates and baselines existing data without replacing files -03:17 +606: test/import_cli_test.dart: CLI postgres imports reviews generates and baselines existing data without replacing files -03:54 +607: loading test/decimal_division_test.dart -03:55 +607: test/decimal_division_test.dart: decimal division sqlite signed modes and scales match exact integer arithmetic -03:55 +608: test/decimal_division_test.dart: decimal division sqlite exact defaults reject lost digits and zero divisors, NULL propagates -03:55 +609: test/decimal_division_test.dart: decimal division sqlite precision exceeds native division scale without double rounding -03:55 +610: test/decimal_division_test.dart: decimal division sqlite huge remainders avoid overflowing intermediate scaled products -04:16 +611: test/decimal_division_test.dart: decimal division sqlite outermost integer scale and result overflow are explicit -04:17 +612: test/decimal_division_test.dart: decimal division sqlite aggregates windows CTEs predicates and unions compose -04:17 +613: test/decimal_division_test.dart: decimal division sqlite expression assignments and transaction rollback preserve values -04:17 +614: test/decimal_division_test.dart: decimal division sqlite window filtering ordering DISTINCT and pagination keep their SQL order -04:17 +615: test/decimal_division_test.dart: decimal division sqlite windows over grouped aggregates run after HAVING -04:17 +616: test/decimal_division_test.dart: decimal division sqlite window results compose through CTEs sets and correlated subqueries -04:17 +617: test/decimal_division_test.dart: decimal division sqlite batched relations retain per-parent window pagination -04:17 +618: test/decimal_division_test.dart: decimal division postgres signed modes and scales match exact integer arithmetic -04:18 +619: test/decimal_division_test.dart: decimal division postgres exact defaults reject lost digits and zero divisors, NULL propagates -04:18 +620: test/decimal_division_test.dart: decimal division postgres precision exceeds native division scale without double rounding -04:18 +621: test/decimal_division_test.dart: decimal division postgres huge remainders avoid overflowing intermediate scaled products -04:38 +622: test/decimal_division_test.dart: decimal division postgres outermost integer scale and result overflow are explicit -04:38 +623: test/decimal_division_test.dart: decimal division postgres aggregates windows CTEs predicates and unions compose -04:38 +624: test/decimal_division_test.dart: decimal division postgres expression assignments and transaction rollback preserve values -04:38 +625: test/decimal_division_test.dart: decimal division postgres window filtering ordering DISTINCT and pagination keep their SQL order -04:38 +626: test/decimal_division_test.dart: decimal division postgres windows over grouped aggregates run after HAVING -04:38 +627: test/decimal_division_test.dart: decimal division postgres window results compose through CTEs sets and correlated subqueries -04:38 +628: test/decimal_division_test.dart: decimal division postgres batched relations retain per-parent window pagination -04:38 +629: test/decimal_division_test.dart: decimal division postgres volatile operands are evaluated exactly once each -04:38 +630: loading test/cli_test.dart -04:38 +630: test/cli_test.dart: CLI generates checks reviews applies and verifies versioned migration files -05:49 +631: test/cli_test.dart: CLI baselines existing SQLite rows and read-only options reject writes -06:07 +632: test/cli_test.dart: CLI uses PostgreSQL-specific connection configuration for apply and verification -06:48 +633: loading test/decimal_test.dart -06:48 +633: test/decimal_test.dart: exact decimal sqlite generated writes, defaults, patches and projections preserve all digits -06:48 +634: test/decimal_test.dart: exact decimal sqlite numeric ordering, predicates, IN, distinct and stable cursors -06:48 +635: test/decimal_test.dart: exact decimal sqlite sums, min/max, grouped aggregates and sliding windows stay exact -06:48 +636: test/decimal_test.dart: exact decimal sqlite computed CTEs, union results, subqueries and streaming keep decimal semantics -06:48 +637: test/decimal_test.dart: exact decimal sqlite numeric primary keys, foreign keys, joined and batched relations -06:48 +638: test/decimal_test.dart: exact decimal sqlite snapshots, catalog verification and imported declarations retain decimal storage -06:50 +639: test/decimal_test.dart: exact decimal sqlite aggregate intermediate overflow may cancel before a valid final result -06:50 +640: test/decimal_test.dart: exact decimal sqlite historical decimal keys resume backfills without lexical ordering or precision loss -06:50 +641: test/decimal_test.dart: exact decimal sqlite reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -06:50 +642: test/decimal_test.dart: exact decimal sqlite invalid external values fail decoding; later valid queries still work -06:50 +643: test/decimal_test.dart: exact decimal postgres generated writes, defaults, patches and projections preserve all digits -06:50 +644: test/decimal_test.dart: exact decimal postgres numeric ordering, predicates, IN, distinct and stable cursors -06:50 +645: test/decimal_test.dart: exact decimal postgres sums, min/max, grouped aggregates and sliding windows stay exact -06:50 +646: test/decimal_test.dart: exact decimal postgres computed CTEs, union results, subqueries and streaming keep decimal semantics -06:50 +647: test/decimal_test.dart: exact decimal postgres numeric primary keys, foreign keys, joined and batched relations -06:50 +648: test/decimal_test.dart: exact decimal postgres snapshots, catalog verification and imported declarations retain decimal storage -06:51 +649: test/decimal_test.dart: exact decimal postgres aggregate intermediate overflow may cancel before a valid final result -06:51 +650: test/decimal_test.dart: exact decimal postgres historical decimal keys resume backfills without lexical ordering or precision loss -06:51 +651: test/decimal_test.dart: exact decimal postgres reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -06:51 +652: test/decimal_test.dart: exact decimal postgres invalid external values fail decoding; later valid queries still work -06:51 +653: test/decimal_test.dart: decimal SQL rejects drivers without exact operations before execution -06:51 +654: test/decimal_test.dart: SQLite recognizes only column decimal collations, and detects drift -06:51 +655: loading test/watch_test.dart -06:51 +655: test/watch_test.dart: independent native SQLite workers require explicit notification -06:51 +656: test/watch_test.dart: watch sqlite lazy initial snapshot and relevant table invalidation -06:51 +657: test/watch_test.dart: watch sqlite transactions publish one snapshot after commit and none on rollback -06:51 +658: test/watch_test.dart: watch sqlite savepoint changes merge on release and disappear on rollback -06:51 +659: test/watch_test.dart: watch sqlite failed statements, zero-row writes and conflict no-ops do not refresh -06:51 +660: test/watch_test.dart: watch sqlite batch writes and upsert returning publish committed changes -06:51 +661: test/watch_test.dart: watch sqlite joined and nested batch relations track child writes -06:51 +662: test/watch_test.dart: watch sqlite relation predicates, SQL subqueries and CTE sources are dependencies -06:51 +663: test/watch_test.dart: watch sqlite declared cascade deletion invalidates a child-only query -06:51 +664: test/watch_test.dart: watch sqlite CASCADE tracks declared transitive deletion effects -06:51 +665: test/watch_test.dart: watch sqlite SET NULL tracks declared transitive deletion effects -06:51 +666: test/watch_test.dart: watch sqlite explicit JOINs and joined CTE definitions track both physical sources -06:51 +667: test/watch_test.dart: watch sqlite raw SQL declares changed and read tables; explicit invalidation follows transactions -06:51 +668: test/watch_test.dart: watch sqlite paused listeners coalesce writes and stop fetching until resumed -06:51 +669: test/watch_test.dart: watch sqlite invalidation during a read suppresses the stale snapshot and refetches -06:51 +670: test/watch_test.dart: watch sqlite decoding errors are reported and a later write can recover -06:51 +671: test/watch_test.dart: watch sqlite separate subscriptions and views of one driver receive changes -06:51 +672: test/watch_test.dart: watch sqlite watching leased sessions is rejected and cancelled tokens close the stream -06:51 +673: test/watch_test.dart: watch sqlite database close finishes watchers even when a listener is paused -06:51 +674: test/watch_test.dart: watch sqlite cancelling an active watched query interrupts SQL and releases its lease -06:51 +675: test/watch_test.dart: watch postgres lazy initial snapshot and relevant table invalidation -06:51 +676: test/watch_test.dart: watch postgres transactions publish one snapshot after commit and none on rollback -06:51 +677: test/watch_test.dart: watch postgres savepoint changes merge on release and disappear on rollback -06:51 +678: test/watch_test.dart: watch postgres failed statements, zero-row writes and conflict no-ops do not refresh -06:51 +679: test/watch_test.dart: watch postgres batch writes and upsert returning publish committed changes -06:51 +680: test/watch_test.dart: watch postgres joined and nested batch relations track child writes -06:51 +681: test/watch_test.dart: watch postgres relation predicates, SQL subqueries and CTE sources are dependencies -06:52 +682: test/watch_test.dart: watch postgres declared cascade deletion invalidates a child-only query -06:52 +683: test/watch_test.dart: watch postgres CASCADE tracks declared transitive deletion effects -06:52 +684: test/watch_test.dart: watch postgres SET NULL tracks declared transitive deletion effects -06:52 +685: test/watch_test.dart: watch postgres explicit JOINs and joined CTE definitions track both physical sources -06:52 +686: test/watch_test.dart: watch postgres raw SQL declares changed and read tables; explicit invalidation follows transactions -06:52 +687: test/watch_test.dart: watch postgres paused listeners coalesce writes and stop fetching until resumed -06:52 +688: test/watch_test.dart: watch postgres invalidation during a read suppresses the stale snapshot and refetches -06:52 +689: test/watch_test.dart: watch postgres decoding errors are reported and a later write can recover -06:52 +690: test/watch_test.dart: watch postgres separate subscriptions and views of one driver receive changes -06:52 +691: test/watch_test.dart: watch postgres watching leased sessions is rejected and cancelled tokens close the stream -06:52 +692: test/watch_test.dart: watch postgres database close finishes watchers even when a listener is paused -06:52 +693: test/watch_test.dart: watch postgres cancelling an active watched query interrupts SQL and releases its lease -06:52 +694: test/watch_test.dart: watch postgres lost commit acknowledgement triggers a conservative re-read -06:52 +695: test/watch_test.dart: watch postgres independent connections require an explicit external-write notification -06:52 +696: loading test/backfill_test.dart -06:52 +696: test/backfill_test.dart: backfill sqlite chunks pause and resume from a checksummed historical declaration -06:52 +697: test/backfill_test.dart: backfill sqlite failed chunk rolls back both writes and cursor, then resumes after repair -06:52 +698: test/backfill_test.dart: backfill sqlite failed completion proof preserves finished chunks without replay -06:52 +699: test/backfill_test.dart: backfill sqlite frozen upper bound and completion proof detect incompatible new writes -06:52 +700: test/backfill_test.dart: backfill sqlite competing runners share committed chunks without duplicate updates -06:52 +701: test/backfill_test.dart: backfill sqlite lost chunk commit acknowledgement preserves the committed frontier -06:52 +702: test/backfill_test.dart: backfill sqlite old checkpoint tables upgrade on apply; completed reads remain nonmutating -06:52 +703: test/backfill_test.dart: backfill sqlite completion conditions reject numeric results instead of truthiness -06:52 +704: test/backfill_test.dart: backfill sqlite empty input still requires the completion condition -06:52 +705: test/backfill_test.dart: backfill sqlite later ordinary migrations retain group atomicity after a completed backfill -06:52 +706: test/backfill_test.dart: backfill sqlite AFTER triggers cannot silently move keys past a committed cursor -06:52 +707: test/backfill_test.dart: backfill sqlite corrupt checkpoint dimensions and invalid run limits are rejected -06:52 +708: test/backfill_test.dart: backfill sqlite rebuild, chunked backfill and required-column rebuild preserve FK enforcement -06:52 +709: test/backfill_test.dart: backfill sqlite CLI previews non-atomic work and reports a bounded run as incomplete -07:11 +710: test/backfill_test.dart: backfill sqlite temporary tables cannot redirect the historical backfill target -07:11 +711: test/backfill_test.dart: backfill sqlite temporary objects cannot impersonate durable migration metadata -07:11 +712: test/backfill_test.dart: backfill sqlite committed chunks invalidate relevant typed query subscriptions -07:11 +713: test/backfill_test.dart: backfill sqlite real process exit after second chunk write resumes correctly -07:13 +714: test/backfill_test.dart: backfill sqlite real process exit after second chunk commit resumes correctly -07:14 +715: test/backfill_test.dart: backfill postgres chunks pause and resume from a checksummed historical declaration -07:15 +716: test/backfill_test.dart: backfill postgres failed chunk rolls back both writes and cursor, then resumes after repair -07:15 +717: test/backfill_test.dart: backfill postgres failed completion proof preserves finished chunks without replay -07:15 +718: test/backfill_test.dart: backfill postgres frozen upper bound and completion proof detect incompatible new writes -07:15 +719: test/backfill_test.dart: backfill postgres competing runners share committed chunks without duplicate updates -07:15 +720: test/backfill_test.dart: backfill postgres lost chunk commit acknowledgement preserves the committed frontier -07:15 +721: test/backfill_test.dart: backfill postgres old checkpoint tables upgrade on apply; completed reads remain nonmutating -07:15 +722: test/backfill_test.dart: backfill postgres completion conditions reject numeric results instead of truthiness -07:15 +723: test/backfill_test.dart: backfill postgres empty input still requires the completion condition -07:15 +724: test/backfill_test.dart: backfill postgres later ordinary migrations retain group atomicity after a completed backfill -07:15 +725: test/backfill_test.dart: backfill postgres AFTER triggers cannot silently move keys past a committed cursor -07:15 +726: test/backfill_test.dart: backfill postgres corrupt checkpoint dimensions and invalid run limits are rejected -07:15 +727: test/backfill_test.dart: backfill postgres CLI previews non-atomic work and reports a bounded run as incomplete -07:33 +728: test/backfill_test.dart: backfill postgres temporary tables cannot redirect the historical backfill target -07:33 +729: test/backfill_test.dart: backfill postgres temporary objects cannot impersonate durable migration metadata -07:33 +730: test/backfill_test.dart: backfill postgres row security cannot hide unfinished rows from the completion proof -07:33 +731: test/backfill_test.dart: backfill postgres inherited duplicate primary keys cannot advance a cursor -07:33 +732: test/backfill_test.dart: backfill postgres committed chunks invalidate relevant typed query subscriptions -07:34 +733: test/backfill_test.dart: backfill postgres real process exit after second chunk write resumes correctly -07:35 +734: test/backfill_test.dart: backfill postgres real process exit after second chunk commit resumes correctly -07:36 +735: loading test/custom_codec_test.dart -07:36 +735: test/custom_codec_test.dart: (setUpAll) -07:36 +735: test/custom_codec_test.dart: custom codecs sqlite generated custom values round trip through create, select and patch -07:36 +736: test/custom_codec_test.dart: custom codecs sqlite enum labels are stable text and unknown labels fail decoding -07:36 +737: test/custom_codec_test.dart: custom codecs sqlite domain validation and bound parameters apply to writes and predicates -07:36 +738: test/custom_codec_test.dart: custom codecs sqlite JSON documents distinguish SQL NULL, JSON null and string scalars -07:36 +739: test/custom_codec_test.dart: custom codecs sqlite nullable structured codec rejects JSON null instead of masking invalid data -07:36 +740: test/custom_codec_test.dart: custom codecs sqlite custom IDs and domain projections work across joined and batched relations -07:36 +741: test/custom_codec_test.dart: custom codecs sqlite custom values participate in atomic batch writes and native upsert -07:36 +742: test/custom_codec_test.dart: custom codecs sqlite custom IDs and enums survive cursor transport and streaming decoders -07:36 +743: test/custom_codec_test.dart: custom codecs sqlite migration snapshots retain physical storage without executing domain codecs -07:36 +744: test/custom_codec_test.dart: custom codecs postgres generated custom values round trip through create, select and patch -07:36 +745: test/custom_codec_test.dart: custom codecs postgres enum labels are stable text and unknown labels fail decoding -07:37 +746: test/custom_codec_test.dart: custom codecs postgres domain validation and bound parameters apply to writes and predicates -07:37 +747: test/custom_codec_test.dart: custom codecs postgres JSON documents distinguish SQL NULL, JSON null and string scalars -07:37 +748: test/custom_codec_test.dart: custom codecs postgres nullable structured codec rejects JSON null instead of masking invalid data -07:37 +749: test/custom_codec_test.dart: custom codecs postgres custom IDs and domain projections work across joined and batched relations -07:37 +750: test/custom_codec_test.dart: custom codecs postgres custom values participate in atomic batch writes and native upsert -07:37 +751: test/custom_codec_test.dart: custom codecs postgres custom IDs and enums survive cursor transport and streaming decoders -07:37 +752: test/custom_codec_test.dart: custom codecs postgres migration snapshots retain physical storage without executing domain codecs -07:37 +753: test/custom_codec_test.dart: (tearDownAll) -07:37 +753: loading test/plan_test.dart -07:37 +753: test/plan_test.dart: inspect sqlite reports composite key width and validates each potential batch -07:37 +754: test/plan_test.dart: inspect sqlite compiles nested plans without acquiring or decoding -07:37 +755: test/plan_test.dart: inspect sqlite distinguishes same-statement joins and skipped batches -07:37 +756: test/plan_test.dart: inspect sqlite retains CTE, UNION and raw SQL boundaries -07:37 +757: test/plan_test.dart: plan execution sqlite one-key SQL templates match executed SQL and internal slots -07:37 +758: test/plan_test.dart: inspect postgres reports composite key width and validates each potential batch -07:37 +759: test/plan_test.dart: inspect postgres compiles nested plans without acquiring or decoding -07:37 +760: test/plan_test.dart: inspect postgres distinguishes same-statement joins and skipped batches -07:37 +761: test/plan_test.dart: inspect postgres retains CTE, UNION and raw SQL boundaries -07:37 +762: test/plan_test.dart: plan execution postgres one-key SQL templates match executed SQL and internal slots -07:37 +763: loading test/check_test.dart -07:37 +763: test/check_test.dart: CHECK sqlite generated named and unnamed constraints enforce values and permit NULL -07:37 +764: test/check_test.dart: CHECK sqlite import preserves names and SQL then generates a matching snapshot -07:39 +765: test/check_test.dart: CHECK sqlite add, change and remove constraints validate old rows atomically -07:39 +766: test/check_test.dart: CHECK sqlite duplicate unnamed checks consume and drop exactly one catalog entry -07:39 +767: test/check_test.dart: CHECK sqlite explicit checked renames preserve rows, incoming references, indexes and views -07:39 +768: test/check_test.dart: CHECK sqlite verification preserves casts, precedence, literals and trailing comments -07:39 +769: test/check_test.dart: CHECK sqlite quoted Unicode identifiers retain SQLite case distinctions -07:39 +770: test/check_test.dart: CHECK sqlite undeclared CHECK is refused before a rebuild and remains enforced -07:39 +771: test/check_test.dart: CHECK postgres generated named and unnamed constraints enforce values and permit NULL -07:39 +772: test/check_test.dart: CHECK postgres import preserves names and SQL then generates a matching snapshot -07:40 +773: test/check_test.dart: CHECK postgres add, change and remove constraints validate old rows atomically -07:40 +774: test/check_test.dart: CHECK postgres duplicate unnamed checks consume and drop exactly one catalog entry -07:40 +775: test/check_test.dart: CHECK postgres explicit checked renames preserve rows, incoming references, indexes and views -07:40 +776: test/check_test.dart: CHECK postgres verification preserves casts, precedence, literals and trailing comments -07:40 +777: test/check_test.dart: CHECK postgres catalog excludes unvalidated and NO INHERIT constraints -07:40 +778: test/check_test.dart: CHECK postgres verification plans but does not execute volatile row expressions -07:40 +779: test/check_test.dart: dialect overrides only alter the affected backend and preserve old snapshot JSON -07:40 +780: loading test/temporal_test.dart -07:40 +780: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: false -07:40 +781: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: true -07:40 +782: test/temporal_test.dart: Gregorian cycles, era boundaries and full finite date range -07:40 +783: test/temporal_test.dart: local time precision, day endpoint and timestamp arithmetic -07:40 +784: test/temporal_test.dart: PostgreSQL binary registry covers boundaries without DateTime -07:40 +785: test/temporal_test.dart: local temporal sqlite generated writes, defaults, null patches and typed parameters -07:40 +786: test/temporal_test.dart: local temporal sqlite finite extremes and microseconds survive native rows and streams -07:40 +787: test/temporal_test.dart: local temporal sqlite calendar ordering, min/max, grouping and stable date cursors -07:40 +788: test/temporal_test.dart: local temporal sqlite equivalent date keys drive batched relations and unique indexes -07:40 +789: test/temporal_test.dart: local temporal sqlite time and local timestamp comparisons retain calendar semantics -07:40 +790: test/temporal_test.dart: local temporal sqlite reviewed text-to-time migration rolls back duplicate clock values -07:40 +791: test/temporal_test.dart: local temporal sqlite invalid external dates fail typed reads without damaging the connection -07:40 +792: test/temporal_test.dart: local temporal sqlite snapshots, catalog import and regenerated defaults agree -07:42 +793: test/temporal_test.dart: local temporal sqlite historical date keys resume a bounded backfill in calendar order -07:42 +794: test/temporal_test.dart: local temporal postgres catalog retains time-truncating casts in expression defaults -07:43 +795: test/temporal_test.dart: local temporal postgres generated writes, defaults, null patches and typed parameters -07:43 +796: test/temporal_test.dart: local temporal postgres finite extremes and microseconds survive native rows and streams -07:43 +797: test/temporal_test.dart: local temporal postgres calendar ordering, min/max, grouping and stable date cursors -07:43 +798: test/temporal_test.dart: local temporal postgres equivalent date keys drive batched relations and unique indexes -07:43 +799: test/temporal_test.dart: local temporal postgres time and local timestamp comparisons retain calendar semantics -07:43 +800: test/temporal_test.dart: local temporal postgres reviewed text-to-time migration rolls back duplicate clock values -07:43 +801: test/temporal_test.dart: local temporal postgres invalid external dates fail typed reads without damaging the connection -07:43 +802: test/temporal_test.dart: local temporal postgres snapshots, catalog import and regenerated defaults agree -07:43 +803: test/temporal_test.dart: local temporal postgres historical date keys resume a bounded backfill in calendar order -07:43 +804: loading test/unconstrained_relation_test.dart -07:43 +804: test/unconstrained_relation_test.dart: unconstrained sqlite catalogs contain no FK, implicit index or uniqueness promise -07:44 +805: test/unconstrained_relation_test.dart: unconstrained sqlite adding and removing a real FK remains a reviewed physical migration -07:44 +806: test/unconstrained_relation_test.dart: unconstrained sqlite unique joins preserve root rows, NULL components and missing targets -07:44 +807: test/unconstrained_relation_test.dart: unconstrained sqlite required target and filter visibility remain explicit -07:44 +808: test/unconstrained_relation_test.dart: unconstrained sqlite nonunique composite collections batch by tuple and page per parent -07:44 +809: test/unconstrained_relation_test.dart: unconstrained sqlite to-one uses cardinality checks without assuming a unique lookup -07:44 +810: test/unconstrained_relation_test.dart: unconstrained sqlite correlated filters and counts do not materialize related rows -07:44 +811: test/unconstrained_relation_test.dart: unconstrained sqlite self relations and inverse collections share the existing planner -07:44 +812: test/unconstrained_relation_test.dart: unconstrained sqlite delete has no cascade and watches track the joined target -07:44 +813: test/unconstrained_relation_test.dart: unconstrained sqlite transaction reads observe writes and outer rollback restores targets -07:44 +814: test/unconstrained_relation_test.dart: unconstrained sqlite streaming preserves optional and batched relation result shapes -07:44 +815: test/unconstrained_relation_test.dart: unconstrained sqlite floating relation keys retain storage intent and key equality -07:44 +816: test/unconstrained_relation_test.dart: unconstrained postgres catalogs contain no FK, implicit index or uniqueness promise -07:44 +817: test/unconstrained_relation_test.dart: unconstrained postgres adding and removing a real FK remains a reviewed physical migration -07:44 +818: test/unconstrained_relation_test.dart: unconstrained postgres unique joins preserve root rows, NULL components and missing targets -07:44 +819: test/unconstrained_relation_test.dart: unconstrained postgres required target and filter visibility remain explicit -07:44 +820: test/unconstrained_relation_test.dart: unconstrained postgres nonunique composite collections batch by tuple and page per parent -07:44 +821: test/unconstrained_relation_test.dart: unconstrained postgres to-one uses cardinality checks without assuming a unique lookup -07:44 +822: test/unconstrained_relation_test.dart: unconstrained postgres correlated filters and counts do not materialize related rows -07:44 +823: test/unconstrained_relation_test.dart: unconstrained postgres self relations and inverse collections share the existing planner -07:44 +824: test/unconstrained_relation_test.dart: unconstrained postgres delete has no cascade and watches track the joined target -07:44 +825: test/unconstrained_relation_test.dart: unconstrained postgres transaction reads observe writes and outer rollback restores targets -07:44 +826: test/unconstrained_relation_test.dart: unconstrained postgres streaming preserves optional and batched relation result shapes -07:44 +827: test/unconstrained_relation_test.dart: unconstrained postgres floating relation keys retain storage intent and key equality -07:44 +828: loading test/decimal_value_test.dart -07:44 +828: test/decimal_value_test.dart: normalization, scientific notation, equality and hash are exact -07:44 +829: test/decimal_value_test.dart: invalid and approximate inputs fail instead of silently rounding -07:44 +830: test/decimal_value_test.dart: range boundaries are checked without binary numeric conversions -07:44 +831: test/decimal_value_test.dart: addition, subtraction, multiplication and ordering keep every digit -07:44 +832: test/decimal_value_test.dart: division requires an explicit scale and rejects implicit rounding -07:44 +833: test/decimal_value_test.dart: rounding modes handle negative values, ties and negative scales -07:44 +834: test/decimal_value_test.dart: small fixed-scale arithmetic agrees with independent integer arithmetic -07:44 +835: All tests passed! diff --git a/research/validation/sqlite-refactor-analyze.log b/research/validation/sqlite-refactor-analyze.log deleted file mode 100644 index 50990799..00000000 --- a/research/validation/sqlite-refactor-analyze.log +++ /dev/null @@ -1,2 +0,0 @@ -Analyzing dart-orm... -No issues found! diff --git a/research/validation/sqlite-refactor-assets-check.log b/research/validation/sqlite-refactor-assets-check.log deleted file mode 100644 index 5baccb1c..00000000 --- a/research/validation/sqlite-refactor-assets-check.log +++ /dev/null @@ -1 +0,0 @@ -Running build hooks...Running build hooks...SQLite web source, worker and WASM fingerprints match. diff --git a/research/validation/sqlite-refactor-browser-js.json b/research/validation/sqlite-refactor-browser-js.json deleted file mode 100644 index f89d99de..00000000 --- a/research/validation/sqlite-refactor-browser-js.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 64 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 41 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 16 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 21 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 19 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 33 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 26 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 4 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 2 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 156 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 29 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 33 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 98 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 22 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "js", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/sqlite-refactor-browser-wasm.json b/research/validation/sqlite-refactor-browser-wasm.json deleted file mode 100644 index 48fb576c..00000000 --- a/research/validation/sqlite-refactor-browser-wasm.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 57 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 39 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 13 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 19 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 19 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 23 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 2 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 155 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 34 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 99 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 13 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "wasm", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" -} diff --git a/research/validation/sqlite-refactor-flutter-example.log b/research/validation/sqlite-refactor-flutter-example.log deleted file mode 100644 index 022746bd..00000000 --- a/research/validation/sqlite-refactor-flutter-example.log +++ /dev/null @@ -1,10 +0,0 @@ - -┌─ New feature ────────────────────────────────────────────────────────────────────────────┐ -│ WebAssembly compilation is new. Understand the details before deploying to production. │ -│ See https://flutter.dev/to/wasm for more information. │ -└──────────────────────────────────────────────────────────────────────────────────────────┘ -Compiling lib/main.dart for the Web... -Expected to find fonts for (MaterialIcons, packages/cupertino_icons/CupertinoIcons), but found (MaterialIcons). This usually means you are referring to font families in an IconData class but not including them in the assets section of your pubspec.yaml, are missing the package that would include them, or are missing "uses-material-design: true". -Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7928 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app. -Compiling lib/main.dart for the Web... 18.8s -✓ Built build/web diff --git a/research/validation/sqlite-refactor-flutter-web.json b/research/validation/sqlite-refactor-flutter-web.json deleted file mode 100644 index c5399e61..00000000 --- a/research/validation/sqlite-refactor-flutter-web.json +++ /dev/null @@ -1,509 +0,0 @@ -[ - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 91 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 39 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 14 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 21 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 18 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 30 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 26 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 3 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 31 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 36 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 102 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 19 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": false, - "flutterFrames": 5, - "baseUri": "http://127.0.0.1:64609/nested/", - "dartUriBase": "http://127.0.0.1:64609/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/nested/main.dart.js", - "/favicon.ico", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/nested/main.dart.js", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/report" - ], - "build": "js", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 85 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 40 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 12 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 19 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 24 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 157 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 32 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 35 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 104 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 12 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 3, - "baseUri": "http://127.0.0.1:64638/nested/", - "dartUriBase": "http://127.0.0.1:64638/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/favicon.ico", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/report" - ], - "build": "wasm", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 54 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 40 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 13 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 21 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 21 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 26 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 0 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 31 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 35 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 103 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 15 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 4, - "baseUri": "http://127.0.0.1:64655/nested/", - "dartUriBase": "http://127.0.0.1:64655/nested/detail/42?phase=recover", - "isolated": true, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/favicon.ico", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/canvaskit/skwasm.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "/nested/canvaskit/skwasm.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/report" - ], - "build": "wasm", - "expectedIsolation": true, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - } -] diff --git a/research/validation/sqlite-refactor-focused.log b/research/validation/sqlite-refactor-focused.log deleted file mode 100644 index 01feeca0..00000000 --- a/research/validation/sqlite-refactor-focused.log +++ /dev/null @@ -1,8 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/sqlite_options_test.dart -00:00 +0: test/sqlite_options_test.dart: persistent uses the explicit native path and retains committed data -00:00 +1: test/sqlite_options_test.dart: native persistent storage never guesses a path or falls back to memory -00:00 +2: test/sqlite_options_test.dart: all driver close callers wait for active work and shutdown -00:00 +3: loading test/public_api_test.dart -00:00 +3: test/public_api_test.dart: generated clients expose row types and an immutable schema -00:00 +4: test/public_api_test.dart: entrypoint exports and runtime dependency boundaries stay explicit -00:06 +5: All tests passed! diff --git a/research/validation/sqlite-refactor-native.log b/research/validation/sqlite-refactor-native.log deleted file mode 100644 index 25289e8a..00000000 --- a/research/validation/sqlite-refactor-native.log +++ /dev/null @@ -1,941 +0,0 @@ -Running build hooks...Running build hooks...00:00 +0: loading test/client_default_test.dart -00:00 +0: test/client_default_test.dart: client defaults sqlite omitted values use typed Dart factories and return stored records -00:00 +1: test/client_default_test.dart: client defaults sqlite explicit values, null and SQL DEFAULT bypass their client factory -00:00 +2: test/client_default_test.dart: client defaults sqlite prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +3: test/client_default_test.dart: client defaults sqlite batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +4: test/client_default_test.dart: client defaults sqlite updates and conflict updates do not reset omitted client values -00:00 +5: test/client_default_test.dart: client defaults sqlite factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +6: test/client_default_test.dart: client defaults sqlite snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +7: test/client_default_test.dart: client defaults sqlite explicit DEFAULT can choose database identity instead of the client value -00:00 +8: test/client_default_test.dart: client defaults postgres omitted values use typed Dart factories and return stored records -00:00 +9: test/client_default_test.dart: client defaults postgres explicit values, null and SQL DEFAULT bypass their client factory -00:00 +10: test/client_default_test.dart: client defaults postgres prepared mutations, repeated compilation and conflicts retain one generated value -00:00 +11: test/client_default_test.dart: client defaults postgres batch insert prepares defaults once per omitted row and preserves explicit values -00:00 +12: test/client_default_test.dart: client defaults postgres updates and conflict updates do not reset omitted client values -00:00 +13: test/client_default_test.dart: client defaults postgres factory errors issue no writes, and rollback does not reverse Dart side effects -00:00 +14: test/client_default_test.dart: client defaults postgres snapshots, migrations, catalogs and imports never store or run a client factory -00:00 +15: test/client_default_test.dart: client defaults postgres explicit DEFAULT can choose database identity instead of the client value -00:00 +16: test/client_default_test.dart: generation reads factory references without executing them and supports moved output -00:02 +17: loading test/decimal_precision_test.dart -00:02 +17: test/decimal_precision_test.dart: decimal precision sqlite column coercion rounds signed ties and handles negative and excess scales -00:02 +18: test/decimal_precision_test.dart: decimal precision sqlite writes constrain columns while arithmetic and wider aggregates retain their result precision -00:02 +19: test/decimal_precision_test.dart: decimal precision sqlite batch inserts and upserts use column coercion and roll back as a unit -00:02 +20: test/decimal_precision_test.dart: decimal precision sqlite catalogs snapshots and imported declarations retain precision scale and defaults -00:04 +21: test/decimal_precision_test.dart: decimal precision sqlite reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:04 +22: test/decimal_precision_test.dart: decimal precision sqlite physical renames and changing constrained defaults preserve column contracts -00:04 +23: test/decimal_precision_test.dart: decimal precision sqlite maximum declared precision and scale bounds remain exact -00:04 +24: test/decimal_precision_test.dart: decimal precision sqlite historical backfills retain constrained decimal key declarations -00:04 +25: test/decimal_precision_test.dart: decimal precision postgres column coercion rounds signed ties and handles negative and excess scales -00:04 +26: test/decimal_precision_test.dart: decimal precision postgres writes constrain columns while arithmetic and wider aggregates retain their result precision -00:05 +27: test/decimal_precision_test.dart: decimal precision postgres batch inserts and upserts use column coercion and roll back as a unit -00:05 +28: test/decimal_precision_test.dart: decimal precision postgres catalogs snapshots and imported declarations retain precision scale and defaults -00:05 +29: test/decimal_precision_test.dart: decimal precision postgres reviewed scale reduction detects rounded duplicate keys and rolls back schema history -00:05 +30: test/decimal_precision_test.dart: decimal precision postgres physical renames and changing constrained defaults preserve column contracts -00:05 +31: test/decimal_precision_test.dart: decimal precision postgres maximum declared precision and scale bounds remain exact -00:05 +32: test/decimal_precision_test.dart: decimal precision postgres historical backfills retain constrained decimal key declarations -00:05 +33: test/decimal_precision_test.dart: SQLite enforces stored precision and checks default coercion and trusted-schema behavior -00:05 +34: test/decimal_precision_test.dart: generation validates DecimalDigits and retains custom decimal codec types -00:08 +35: test/decimal_precision_test.dart: metadata rejects invalid precisions scales and non-decimal columns -00:08 +36: loading test/selection_test.dart -00:08 +36: test/selection_test.dart: selection sqlite all typed arities defer mapping and preserve left-to-right evaluation -00:08 +37: test/selection_test.dart: selection sqlite first decoder failure prevents later fields and outer mapper -00:08 +38: test/selection_test.dart: selection sqlite dynamic fields snapshot keys and preserve nulls and mapping order -00:08 +39: test/selection_test.dart: selection postgres all typed arities defer mapping and preserve left-to-right evaluation -00:08 +40: test/selection_test.dart: selection postgres first decoder failure prevents later fields and outer mapper -00:08 +41: test/selection_test.dart: selection postgres dynamic fields snapshot keys and preserve nulls and mapping order -00:08 +42: loading test/build_runner_test.dart -00:08 +42: test/build_runner_test.dart: real build_runner build/watch tracks imports, recovers errors and deletes stale outputs -00:34 +43: loading test/schema_version_test.dart -00:34 +43: test/schema_version_test.dart: schema version sqlite invalid ranges fail before SQL; caller transactions stay usable -00:34 +44: test/schema_version_test.dart: schema version sqlite unversioned existing data is rejected without creating history -00:34 +45: test/schema_version_test.dart: schema version sqlite default requires latest; inclusive compatibility range is explicit -00:34 +46: test/schema_version_test.dart: schema version sqlite newer database rejects downgrade without changing data or schema -00:34 +47: test/schema_version_test.dart: schema version sqlite complete applied prefix is checked, including old checksums and missing entries -00:34 +48: test/schema_version_test.dart: schema version sqlite matching version does not claim that the catalog is free of drift -00:34 +49: test/schema_version_test.dart: schema version sqlite verified baseline is compatible without replaying creation -00:34 +50: test/schema_version_test.dart: schema version sqlite borrowed session uses a read snapshot and survives rejected versions -00:34 +51: test/schema_version_test.dart: schema version postgres invalid ranges fail before SQL; caller transactions stay usable -00:34 +52: test/schema_version_test.dart: schema version postgres unversioned existing data is rejected without creating history -00:34 +53: test/schema_version_test.dart: schema version postgres default requires latest; inclusive compatibility range is explicit -00:34 +54: test/schema_version_test.dart: schema version postgres newer database rejects downgrade without changing data or schema -00:34 +55: test/schema_version_test.dart: schema version postgres complete applied prefix is checked, including old checksums and missing entries -00:34 +56: test/schema_version_test.dart: schema version postgres matching version does not claim that the catalog is free of drift -00:34 +57: test/schema_version_test.dart: schema version postgres verified baseline is compatible without replaying creation -00:34 +58: test/schema_version_test.dart: schema version postgres borrowed session uses a read snapshot and survives rejected versions -00:34 +59: test/schema_version_test.dart: schema version postgres unfinished recoverable migration blocks startup until repaired and completed -00:34 +60: test/schema_version_test.dart: schema version postgres catalog and history share a snapshot and the supplied history is frozen -00:34 +61: test/schema_version_test.dart: SQLite version gate works on an explicitly read-only file -00:34 +62: loading test/types_test.dart -00:34 +62: test/types_test.dart: junction-table keys, enum payloads and nested results keep generated types -00:36 +63: test/types_test.dart: computed generated fields are statically readable and never writable -00:37 +64: test/types_test.dart: client-default create parameters preserve domain, nullable and timestamp types -00:37 +65: test/types_test.dart: query-only relation APIs retain result types and expose no graph writes -00:37 +66: test/types_test.dart: calendar APIs reject instants, strings and mixed temporal types -00:38 +67: test/types_test.dart: actual generated APIs reject wrong inputs, results and backend options -00:38 +68: loading test/migration_test.dart -00:38 +68: test/migration_test.dart: (setUpAll) -00:38 +68: test/migration_test.dart: migrations sqlite explicit table and column renames preserve incoming references and data -00:38 +69: test/migration_test.dart: migrations sqlite constraint changes resolve catalog names and retain rows -00:38 +70: test/migration_test.dart: migrations sqlite failed NOT NULL rebuild restores data history and foreign key settings -00:38 +71: test/migration_test.dart: migrations sqlite reviewed conversion expressions preserve values when storage types change -00:38 +72: test/migration_test.dart: migrations sqlite additive changes use ALTER without a table rebuild -00:38 +73: test/migration_test.dart: migrations sqlite reviewed table removal handles restrictive foreign keys atomically -00:38 +74: test/migration_test.dart: migrations sqlite broken migration chain is refused before DDL -00:38 +75: test/migration_test.dart: migrations sqlite catalog distinguishes nullable non-rowid primary keys -00:38 +76: test/migration_test.dart: migrations sqlite rebuild preserves unmanaged index trigger and view definitions -00:38 +77: test/migration_test.dart: migrations sqlite foreign key check rolls back changed parent keys without cascading data away -00:38 +78: test/migration_test.dart: migrations sqlite unmodeled CHECK constraints are refused without losing their definition -00:38 +79: test/migration_test.dart: migrations postgres explicit table and column renames preserve incoming references and data -00:38 +80: test/migration_test.dart: migrations postgres constraint changes resolve catalog names and retain rows -00:38 +81: test/migration_test.dart: migrations postgres failed NOT NULL rebuild restores data history and foreign key settings -00:39 +82: test/migration_test.dart: migrations postgres reviewed conversion expressions preserve values when storage types change -00:39 +83: test/migration_test.dart: migrations postgres additive changes use ALTER without a table rebuild -00:39 +84: test/migration_test.dart: migrations postgres reviewed table removal handles restrictive foreign keys atomically -00:39 +85: test/migration_test.dart: migrations postgres broken migration chain is refused before DDL -00:39 +86: test/migration_test.dart: diff requires explicit renames, casts and backfills -00:39 +87: test/migration_test.dart: (tearDownAll) -00:39 +87: loading test/many_to_many_test.dart -00:39 +87: test/many_to_many_test.dart: many-to-many sqlite two declared FKs and a composite primary key enforce association identity -00:39 +88: test/many_to_many_test.dart: many-to-many sqlite forward projection keeps payload, shared targets and per-user limits in two statements -00:39 +89: test/many_to_many_test.dart: many-to-many sqlite reverse projection applies tie-breaking and offset to each team without changing root pagination -00:39 +90: test/many_to_many_test.dart: many-to-many sqlite nested team rosters deduplicate shared team keys and batch by level -00:39 +91: test/many_to_many_test.dart: many-to-many sqlite association predicates and counts stay in one SQL statement -00:39 +92: test/many_to_many_test.dart: many-to-many sqlite limited parameter capacity chunks associations without changing per-parent limits -00:39 +93: test/many_to_many_test.dart: many-to-many sqlite transaction rollback restores payload changes and new endpoints after an invalid association -00:39 +94: test/many_to_many_test.dart: many-to-many sqlite composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:39 +95: test/many_to_many_test.dart: many-to-many sqlite watch tracks junction payload and opposite endpoint changes after commit -00:39 +96: test/many_to_many_test.dart: many-to-many postgres two declared FKs and a composite primary key enforce association identity -00:39 +97: test/many_to_many_test.dart: many-to-many postgres forward projection keeps payload, shared targets and per-user limits in two statements -00:39 +98: test/many_to_many_test.dart: many-to-many postgres reverse projection applies tie-breaking and offset to each team without changing root pagination -00:39 +99: test/many_to_many_test.dart: many-to-many postgres nested team rosters deduplicate shared team keys and batch by level -00:39 +100: test/many_to_many_test.dart: many-to-many postgres association predicates and counts stay in one SQL statement -00:39 +101: test/many_to_many_test.dart: many-to-many postgres limited parameter capacity chunks associations without changing per-parent limits -00:39 +102: test/many_to_many_test.dart: many-to-many postgres transaction rollback restores payload changes and new endpoints after an invalid association -00:39 +103: test/many_to_many_test.dart: many-to-many postgres composite-key conflict updates preserve membership dates and unlinking keeps endpoints -00:39 +104: test/many_to_many_test.dart: many-to-many postgres watch tracks junction payload and opposite endpoint changes after commit -00:39 +105: loading test/migration_target_test.dart -00:39 +105: test/migration_target_test.dart: history requires a single engine even when empty or SQL is portable -00:39 +106: test/migration_target_test.dart: unselected expressions cannot change a saved plan, hash or source -00:39 +107: test/migration_target_test.dart: SQLite virtual indexes and computed mode changes ignore PostgreSQL limits -00:39 +108: test/migration_target_test.dart: replacing stored values with computed values requires explicit review -00:39 +109: test/migration_target_test.dart: older PostgreSQL is rejected before locks, journals or partial execution -00:39 +110: test/migration_target_test.dart: registry fixes its engine at initialization and refuses retargeting -00:39 +111: test/migration_target_test.dart: wrong engine fails before any migration SQL for runner and every connected CLI command -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -OrmException(MIGRATION.TARGET): History targets postgres, connection is sqlite. -00:40 +112: test/migration_target_test.dart: PostgreSQL applies only its own generated conversion and rejects a SQLite history before SQL -00:40 +113: loading test/stream_test.dart -00:40 +113: test/stream_test.dart: sqlite cancellation between batch chunks prevents a partial commit -00:40 +114: test/stream_test.dart: sqlite cancellation interrupts an active cursor fetch -00:40 +115: test/stream_test.dart: sqlite timed-out mutations return no partial write -00:40 +116: test/stream_test.dart: sqlite database close stops a paused cursor and releases the driver -00:40 +117: test/stream_test.dart: sqlite cancellation between stream creation and listening needs no rollback -00:40 +118: test/stream_test.dart: sqlite bounded cursor fetches preserve parameters and typed rows -00:40 +119: test/stream_test.dart: sqlite pause stops fetches and early cancellation releases the lease -00:40 +120: test/stream_test.dart: sqlite await-for provides demand and take closes without exhausting -00:40 +121: test/stream_test.dart: sqlite relation projections load once per root batch -00:40 +122: test/stream_test.dart: sqlite mapper failure closes the cursor and rolls back -00:40 +123: test/stream_test.dart: sqlite early stream end inside a transaction preserves other work -00:40 +124: test/stream_test.dart: sqlite escaping paused stream cannot keep a transaction alive -00:40 +125: test/stream_test.dart: sqlite cancels executing SQL and allows the next statement -00:40 +126: test/stream_test.dart: sqlite statement timeout stops SQL and leaves a usable connection -00:40 +127: test/stream_test.dart: sqlite cancelled statement poisons a transaction even if caught -00:40 +128: test/stream_test.dart: sqlite cancelling a paused stream releases its connection before resume -00:40 +129: test/stream_test.dart: sqlite cancellation before starting performs no SQL -00:40 +130: test/stream_test.dart: postgres cancellation between batch chunks prevents a partial commit -00:40 +131: test/stream_test.dart: postgres cancellation interrupts an active cursor fetch -00:40 +132: test/stream_test.dart: postgres timed-out mutations return no partial write -00:40 +133: test/stream_test.dart: postgres database close stops a paused cursor and releases the driver -00:40 +134: test/stream_test.dart: postgres cancellation between stream creation and listening needs no rollback -00:40 +135: test/stream_test.dart: postgres bounded cursor fetches preserve parameters and typed rows -00:40 +136: test/stream_test.dart: postgres pause stops fetches and early cancellation releases the lease -00:40 +137: test/stream_test.dart: postgres await-for provides demand and take closes without exhausting -00:40 +138: test/stream_test.dart: postgres relation projections load once per root batch -00:40 +139: test/stream_test.dart: postgres mapper failure closes the cursor and rolls back -00:40 +140: test/stream_test.dart: postgres early stream end inside a transaction preserves other work -00:40 +141: test/stream_test.dart: postgres escaping paused stream cannot keep a transaction alive -00:40 +142: test/stream_test.dart: postgres cancels executing SQL and allows the next statement -00:41 +143: test/stream_test.dart: postgres statement timeout stops SQL and leaves a usable connection -00:41 +144: test/stream_test.dart: postgres cancelled statement poisons a transaction even if caught -00:41 +145: test/stream_test.dart: postgres cancelling a paused stream releases its connection before resume -00:41 +146: test/stream_test.dart: postgres cancellation before starting performs no SQL -00:41 +147: test/stream_test.dart: late PostgreSQL cancellation cannot reach the next statement -00:41 +148: test/stream_test.dart: borrowed pool requires cancellation support and retains ownership -00:41 +149: test/stream_test.dart: failed cancellation control discards the physical connection -00:41 +150: loading test/migration_recovery_test.dart -00:41 +150: test/migration_recovery_test.dart: (setUpAll) -00:41 +150: test/migration_recovery_test.dart: SQLite and transaction controls reject recoverable autocommit steps -00:41 +151: test/migration_recovery_test.dart: mixed migrations checkpoint SQL and verify concurrent indexes -00:51 +152: test/migration_recovery_test.dart: same-name wrong-definition indexes are never accepted as completion -00:51 +153: test/migration_recovery_test.dart: failed unique builds leave INVALID indexes and require explicit repair -00:51 +154: test/migration_recovery_test.dart: failed attempts freeze migration checksums and block baseline -00:51 +155: test/migration_recovery_test.dart: failed postconditions report their actual failing stage -00:51 +156: test/migration_recovery_test.dart: recovery probes must produce one boolean value -00:51 +157: test/migration_recovery_test.dart: concurrent runners serialize normal and autocommit steps under one lock -00:51 +158: test/migration_recovery_test.dart: runner lock timeout leaves history untouched and the connection reusable -00:51 +159: test/migration_recovery_test.dart: ordinary SQL inside a mixed migration rolls back only its failed step and resumes -00:52 +160: test/migration_recovery_test.dart: completion checks reject partial and NULLS NOT DISTINCT definitions -00:52 +161: test/migration_recovery_test.dart: real process exit after concurrent DDL resumes without replay -00:54 +162: test/migration_recovery_test.dart: real process exit after transaction commit resumes without replay -00:56 +163: test/migration_recovery_test.dart: (tearDownAll) -00:56 +163: loading test/acquisition_test.dart -00:56 +163: test/acquisition_test.dart: an expired lease cannot enter before a delayed Timer gets its turn -00:56 +164: test/acquisition_test.dart: acquisition sqlite expired raw writes never execute after a late lease becomes available -00:56 +165: test/acquisition_test.dart: acquisition sqlite query cancellation returns while the pool is still occupied -00:56 +166: test/acquisition_test.dart: acquisition sqlite typed mutations and batch transactions propagate acquisition limits -00:56 +167: test/acquisition_test.dart: acquisition sqlite expired transactions and sessions never enter their callbacks -00:56 +168: test/acquisition_test.dart: acquisition sqlite acquisition timeout stops on entry and statement timeout starts afterward -00:56 +169: test/acquisition_test.dart: acquisition sqlite invalid and already-cancelled acquisition options never queue work -00:56 +170: test/acquisition_test.dart: acquisition sqlite stream cancellation and timeout return without opening a cursor -00:56 +171: test/acquisition_test.dart: acquisition sqlite query watch acquisition failure can recover on a later invalidation -00:56 +172: test/acquisition_test.dart: acquisition sqlite close drains abandoned reservations without running their callbacks -00:56 +173: test/acquisition_test.dart: acquisition sqlite abandoned requests preserve pool reuse and subsequent action errors -00:56 +174: test/acquisition_test.dart: PostgreSQL native pool timeout is classified before entering the callback -00:57 +175: test/acquisition_test.dart: PostgreSQL connection establishment timeout does not cap pool waiting -00:57 +176: test/acquisition_test.dart: acquisition postgres expired raw writes never execute after a late lease becomes available -00:57 +177: test/acquisition_test.dart: acquisition postgres query cancellation returns while the pool is still occupied -00:57 +178: test/acquisition_test.dart: acquisition postgres typed mutations and batch transactions propagate acquisition limits -00:57 +179: test/acquisition_test.dart: acquisition postgres expired transactions and sessions never enter their callbacks -00:57 +180: test/acquisition_test.dart: acquisition postgres acquisition timeout stops on entry and statement timeout starts afterward -00:57 +181: test/acquisition_test.dart: acquisition postgres invalid and already-cancelled acquisition options never queue work -00:57 +182: test/acquisition_test.dart: acquisition postgres stream cancellation and timeout return without opening a cursor -00:57 +183: test/acquisition_test.dart: acquisition postgres query watch acquisition failure can recover on a later invalidation -00:58 +184: test/acquisition_test.dart: acquisition postgres close drains abandoned reservations without running their callbacks -00:58 +185: test/acquisition_test.dart: acquisition postgres abandoned requests preserve pool reuse and subsequent action errors -00:58 +186: test/acquisition_test.dart: PostgreSQL timeout during real connection initialization releases the late lease -00:58 +187: loading test/computed_test.dart -00:58 +187: test/computed_test.dart: computed sqlite native expressions preserve quoted identifiers, comments and SQL-like string values -00:58 +188: test/computed_test.dart: computed sqlite generated CRUD, projections, batch and conflict writes compute stored and virtual values -00:58 +189: test/computed_test.dart: computed sqlite catalog/import roundtrip preserves SQL and modes and detects drift -01:00 +190: test/computed_test.dart: computed sqlite ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:00 +191: test/computed_test.dart: computed sqlite ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:00 +192: test/computed_test.dart: computed sqlite stored to ordinary materialization retains values and permits later writes -01:00 +193: test/computed_test.dart: computed sqlite explicit renamed expressions preserve indexes, checks, references and views -01:00 +194: test/computed_test.dart: computed sqlite invalid recomputation rolls back the table and migration history -01:00 +195: test/computed_test.dart: computed sqlite decimal computed SQL keeps precision coercion and imports exactly once -01:01 +196: test/computed_test.dart: computed postgres native expressions preserve quoted identifiers, comments and SQL-like string values -01:01 +197: test/computed_test.dart: computed postgres generated CRUD, projections, batch and conflict writes compute stored and virtual values -01:01 +198: test/computed_test.dart: computed postgres catalog/import roundtrip preserves SQL and modes and detects drift -01:01 +199: test/computed_test.dart: computed postgres ComputedStorage.stored add, expression/type change and drop preserve existing rows -01:01 +200: test/computed_test.dart: computed postgres ComputedStorage.virtual add, expression/type change and drop preserve existing rows -01:01 +201: test/computed_test.dart: computed postgres stored to ordinary materialization retains values and permits later writes -01:01 +202: test/computed_test.dart: computed postgres explicit renamed expressions preserve indexes, checks, references and views -01:01 +203: test/computed_test.dart: computed postgres invalid recomputation rolls back the table and migration history -01:01 +204: test/computed_test.dart: computed postgres decimal computed SQL keeps precision coercion and imports exactly once -01:02 +205: test/computed_test.dart: mode conversions need explicit plans and historical copy/backfill cannot write computed columns -01:02 +206: loading test/named_sql_test.dart -01:02 +206: test/named_sql_test.dart: template binds repeated names once and preserves quotes/comments -01:02 +207: test/named_sql_test.dart: templates reject ambiguous bindings and incomplete or multiple statements -01:02 +208: test/named_sql_test.dart: generated client is deterministic and checked against its Dart declaration -01:04 +209: test/named_sql_test.dart: sqlite typed parameters, projection and repeated nullable bindings compose -01:04 +210: test/named_sql_test.dart: sqlite native numeric and temporal codecs survive binding and streaming -01:04 +211: test/named_sql_test.dart: sqlite CTEs and unions retain distinct query arguments and field identity -01:04 +212: test/named_sql_test.dart: sqlite transactions see pending writes and rollback normally -01:04 +213: test/named_sql_test.dart: sqlite named sources reject table mutations and require explicit watch reads -01:04 +214: test/named_sql_test.dart: sqlite native checks accept declared shape and preserve prepared-statement state -01:05 +215: test/named_sql_test.dart: sqlite native checks reject syntax, missing tables and missing result aliases -01:05 +216: test/named_sql_test.dart: sqlite native checks do not evaluate application expressions -01:05 +217: test/named_sql_test.dart: postgres typed parameters, projection and repeated nullable bindings compose -01:05 +218: test/named_sql_test.dart: postgres native numeric and temporal codecs survive binding and streaming -01:05 +219: test/named_sql_test.dart: postgres CTEs and unions retain distinct query arguments and field identity -01:05 +220: test/named_sql_test.dart: postgres transactions see pending writes and rollback normally -01:05 +221: test/named_sql_test.dart: postgres named sources reject table mutations and require explicit watch reads -01:05 +222: test/named_sql_test.dart: postgres native checks accept declared shape and preserve prepared-statement state -01:05 +223: test/named_sql_test.dart: postgres native checks reject syntax, missing tables and missing result aliases -01:05 +224: test/named_sql_test.dart: postgres native checks do not evaluate application expressions -01:05 +225: loading test/named_sql_generation_test.dart -01:05 +225: test/named_sql_generation_test.dart: source contracts reject missing/extra parameters and row metadata -01:10 +226: test/named_sql_generation_test.dart: SQL edits and modified generated code fail freshness checks -01:12 +227: test/named_sql_generation_test.dart: asset builder and standalone generator emit identical SQL contracts -01:13 +228: test/named_sql_generation_test.dart: generated APIs reject missing/wrong parameters, result types and unsupported backends -01:13 +229: test/named_sql_generation_test.dart: CLI checks a persistent SQLite database without creating missing files -01:32 +230: loading test/decimal_average_test.dart -01:32 +230: test/decimal_average_test.dart: integer fractions round once and only bound their final result -01:33 +231: test/decimal_average_test.dart: decimal average sqlite empty null and constant inputs retain aggregate semantics -01:33 +232: test/decimal_average_test.dart: decimal average sqlite all rounding modes match exact signed sums across scales -01:33 +233: test/decimal_average_test.dart: decimal average sqlite valid means survive sums exceeding the finite numeric range -01:35 +234: test/decimal_average_test.dart: decimal average sqlite minimum fractions and outermost rounding scales stay exact -01:35 +235: test/decimal_average_test.dart: decimal average sqlite grouping HAVING ordering and CTE references preserve the mean -01:35 +236: test/decimal_average_test.dart: decimal average sqlite illegal window nesting and aggregate assignments fail before SQL -01:35 +237: test/decimal_average_test.dart: decimal average sqlite windows and rounded window means respect partitions and pagination -01:35 +238: test/decimal_average_test.dart: decimal average sqlite correlated means can update values and failures roll back transactions -01:35 +239: test/decimal_average_test.dart: decimal average sqlite batched relationship windows keep per-parent limits -01:35 +240: test/decimal_average_test.dart: decimal average sqlite sliding windows remove values, reset empty frames and reject varying policies -01:35 +241: test/decimal_average_test.dart: decimal average sqlite window means of grouped sums run after HAVING -01:35 +242: test/decimal_average_test.dart: decimal average postgres empty null and constant inputs retain aggregate semantics -01:35 +243: test/decimal_average_test.dart: decimal average postgres all rounding modes match exact signed sums across scales -01:35 +244: test/decimal_average_test.dart: decimal average postgres valid means survive sums exceeding the finite numeric range -01:35 +245: test/decimal_average_test.dart: decimal average postgres minimum fractions and outermost rounding scales stay exact -01:35 +246: test/decimal_average_test.dart: decimal average postgres grouping HAVING ordering and CTE references preserve the mean -01:35 +247: test/decimal_average_test.dart: decimal average postgres illegal window nesting and aggregate assignments fail before SQL -01:35 +248: test/decimal_average_test.dart: decimal average postgres windows and rounded window means respect partitions and pagination -01:35 +249: test/decimal_average_test.dart: decimal average postgres correlated means can update values and failures roll back transactions -01:35 +250: test/decimal_average_test.dart: decimal average postgres batched relationship windows keep per-parent limits -01:35 +251: test/decimal_average_test.dart: decimal average postgres window means of grouped sums run after HAVING -01:35 +252: test/decimal_average_test.dart: decimal average postgres volatile inputs and window ordering are evaluated once per row -01:35 +253: loading test/public_api_test.dart -01:35 +253: test/public_api_test.dart: generated clients expose row types and an immutable schema -01:35 +254: test/public_api_test.dart: entrypoint exports and runtime dependency boundaries stay explicit -01:41 +255: loading test/instant_test.dart -01:41 +255: test/instant_test.dart: strict UTC parsing handles eras, offsets, endpoints and invalid components -01:41 +256: test/instant_test.dart: UTC defaults and local DateTime inputs do not depend on the process timezone -01:42 +257: test/instant_test.dart: historical migration checksum and timestamp tag remain immutable -01:42 +258: test/instant_test.dart: UTC instant sqlite native round trips and streams preserve exact microseconds and common endpoints -01:42 +259: test/instant_test.dart: UTC instant sqlite chronological comparisons, grouping, windows, union and CTEs use instants -01:42 +260: test/instant_test.dart: UTC instant sqlite offset-equivalent keys join and batch through the same unique index -01:42 +261: test/instant_test.dart: UTC instant sqlite current snapshots, physical catalog and imported DateTime declarations agree -01:44 +262: test/instant_test.dart: UTC instant sqlite offset literals compare by value while expression casts remain visible -01:44 +263: test/instant_test.dart: UTC instant sqlite reviewed migration preserves history and repairs legacy timestamp ordering -01:44 +264: test/instant_test.dart: UTC instant sqlite legacy key collisions and invalid calendar values roll back conversion -01:44 +265: test/instant_test.dart: UTC instant sqlite instant primary-key backfills resume across sub-millisecond boundaries -01:44 +266: test/instant_test.dart: UTC instant sqlite external infinity and out-of-DateTime values fail decoding without wrapping -01:44 +267: test/instant_test.dart: UTC instant postgres native round trips and streams preserve exact microseconds and common endpoints -01:45 +268: test/instant_test.dart: UTC instant postgres chronological comparisons, grouping, windows, union and CTEs use instants -01:45 +269: test/instant_test.dart: UTC instant postgres offset-equivalent keys join and batch through the same unique index -01:45 +270: test/instant_test.dart: UTC instant postgres current snapshots, physical catalog and imported DateTime declarations agree -01:45 +271: test/instant_test.dart: UTC instant postgres offset literals compare by value while expression casts remain visible -01:45 +272: test/instant_test.dart: UTC instant postgres reviewed migration preserves history and repairs legacy timestamp ordering -01:45 +273: test/instant_test.dart: UTC instant postgres instant primary-key backfills resume across sub-millisecond boundaries -01:45 +274: test/instant_test.dart: UTC instant postgres external infinity and out-of-DateTime values fail decoding without wrapping -01:45 +275: loading test/codec_test.dart -01:45 +275: test/codec_test.dart: numeric aggregate text decodes without integer rounding or overflow -01:45 +276: test/codec_test.dart: erased codecs keep checked argument types without function getter casts -01:45 +277: test/codec_test.dart: mapped domain codecs preserve one decode and one encode boundary -01:45 +278: test/codec_test.dart: parsed JSON scalars and document presence have unambiguous decoding -01:45 +279: test/codec_test.dart: enum mappings reject duplicates and do not follow mutable caller maps -01:45 +280: loading test/sql_real_test.dart -01:46 +280: test/sql_real_test.dart: sqlite explicitly typed REAL parameters preserve floating storage -01:46 +281: test/sql_real_test.dart: postgres explicitly typed REAL parameters preserve floating storage -01:46 +282: loading test/migration_source_test.dart -01:46 +282: test/migration_source_test.dart: Dart emission preserves storage metadata and every operation without application imports -01:48 +283: test/migration_source_test.dart: project CLI creates Dart history, upgrades a database and preserves recorded fingerprints -02:58 +284: test/migration_source_test.dart: AOT migration bundle works without Dart sources or migration assets -03:05 +285: loading test/generator_test.dart -03:06 +285: test/generator_test.dart: (setUpAll) -03:06 +285: test/generator_test.dart: target capabilities are checked when choosing a migration engine -03:08 +286: test/generator_test.dart: unusable extension getters and generated symbol collisions fail at generation -03:09 +287: test/generator_test.dart: client and snapshot paths cannot overwrite the declaration -03:09 +288: test/generator_test.dart: record annotations, physical names, relations and snapshots -03:10 +289: test/generator_test.dart: output is deterministic and matches the committed generated client -03:14 +290: test/generator_test.dart: composite keys and more than six fields generate analyzable code -03:15 +291: test/generator_test.dart: computed keys are rejected instead of executing a fake row -03:15 +292: test/generator_test.dart: computed declarations reject conflicting defaults, empty SQL and invalid row identities -03:16 +293: test/generator_test.dart: client defaults reject incompatible, asynchronous, private and ambiguous factories -03:19 +294: test/generator_test.dart: CHECK declarations reject dynamic SQL, empty SQL and duplicate names -03:20 +295: test/generator_test.dart: custom types resolve defining libraries when output moves to another directory -03:20 +296: test/generator_test.dart: nested generic and record types preserve nullable aliases without running codecs -03:21 +297: test/generator_test.dart: invalid custom declaration: wrong_codec -03:21 +298: test/generator_test.dart: invalid custom declaration: nullable_codec -03:21 +299: test/generator_test.dart: invalid custom declaration: erased_extension -03:22 +300: test/generator_test.dart: invalid custom declaration: unknown_storage -03:22 +301: test/generator_test.dart: invalid custom declaration: private_codec -03:22 +302: test/generator_test.dart: invalid custom declaration: private_type -03:22 +303: test/generator_test.dart: invalid custom declaration: inline_codec -03:23 +304: test/generator_test.dart: invalid custom declaration: duplicate_codec -03:23 +305: test/generator_test.dart: invalid custom declaration: missing_codec -03:23 +306: test/generator_test.dart: invalid custom declaration: duplicate_labels -03:23 +307: test/generator_test.dart: invalid custom declaration: duplicate_enum_annotation -03:24 +308: test/generator_test.dart: foreign keys require unique targets -03:24 +309: test/generator_test.dart: query-only edges do not alter physical snapshots or require unique targets -03:24 +310: test/generator_test.dart: constrained and unconstrained edges retain separate migration meaning -03:25 +311: test/generator_test.dart: invalid query-only relation: lookup_computed -03:25 +312: test/generator_test.dart: invalid query-only relation: lookup_codec -03:25 +313: test/generator_test.dart: invalid query-only relation: lookup_inverse -03:25 +314: test/generator_test.dart: invalid query-only relation: lookup_type -03:26 +315: test/generator_test.dart: invalid query-only relation: lookup_delete -03:26 +316: test/generator_test.dart: Dart type errors stop generation -03:26 +317: test/generator_test.dart: duplicate physical names and nullable primary keys are rejected -03:26 +318: test/generator_test.dart: (tearDownAll) -03:26 +318: loading test/transaction_test.dart -03:27 +318: test/transaction_test.dart: transaction control sqlite idle callback expires, rolls back and cannot issue late SQL -03:27 +319: test/transaction_test.dart: transaction control sqlite deadline interrupts active SQL before releasing and reusing the lease -03:27 +320: test/transaction_test.dart: transaction control sqlite interrupted write rolls back and leaves the connection usable -03:27 +321: test/transaction_test.dart: transaction control sqlite explicit transaction cancellation stops an idle callback and active SQL -03:27 +322: test/transaction_test.dart: transaction control sqlite global transaction cancellation also abandons acquisition -03:27 +323: test/transaction_test.dart: transaction control sqlite savepoint inherits deadline, drains and rolls back through raw cleanup -03:27 +324: test/transaction_test.dart: transaction control sqlite deadline closes a paused transactional cursor before rollback -03:27 +325: test/transaction_test.dart: transaction control sqlite deadline interrupts an active transactional cursor fetch -03:27 +326: test/transaction_test.dart: transaction control sqlite transaction clock starts after acquisition and a borrowed session recovers -03:27 +327: test/transaction_test.dart: transaction control sqlite failed rollback is explicit and discards an uncertain connection -03:28 +328: test/transaction_test.dart: transaction control sqlite transaction controls reject adapters without actual cancellation -03:28 +329: test/transaction_test.dart: transaction control sqlite shorter statement limit retains its error and rolls back the transaction -03:28 +330: test/transaction_test.dart: transaction control sqlite monotonic deadline prevents commit after a CPU-bound callback -03:28 +331: test/transaction_test.dart: transaction control sqlite completed scopes remove their timers and cancellation listeners -03:28 +332: test/transaction_test.dart: transaction control sqlite confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:28 +333: test/transaction_test.dart: transaction control sqlite deferred constraint rejection at COMMIT is a known failure -03:28 +334: test/transaction_test.dart: transaction control sqlite invalid deadlines and already cancelled tokens never begin a transaction -03:28 +335: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=false -03:28 +336: test/transaction_test.dart: transaction control sqlite automatic SQLite rollback prevents late autocommit writes, nested=true -03:28 +337: test/transaction_test.dart: transaction control postgres idle callback expires, rolls back and cannot issue late SQL -03:28 +338: test/transaction_test.dart: transaction control postgres deadline interrupts active SQL before releasing and reusing the lease -03:28 +339: test/transaction_test.dart: transaction control postgres interrupted write rolls back and leaves the connection usable -03:28 +340: test/transaction_test.dart: transaction control postgres explicit transaction cancellation stops an idle callback and active SQL -03:28 +341: test/transaction_test.dart: transaction control postgres global transaction cancellation also abandons acquisition -03:28 +342: test/transaction_test.dart: transaction control postgres savepoint inherits deadline, drains and rolls back through raw cleanup -03:29 +343: test/transaction_test.dart: transaction control postgres deadline closes a paused transactional cursor before rollback -03:29 +344: test/transaction_test.dart: transaction control postgres deadline interrupts an active transactional cursor fetch -03:29 +345: test/transaction_test.dart: transaction control postgres transaction clock starts after acquisition and a borrowed session recovers -03:29 +346: test/transaction_test.dart: transaction control postgres failed rollback is explicit and discards an uncertain connection -03:29 +347: test/transaction_test.dart: transaction control postgres transaction controls reject adapters without actual cancellation -03:29 +348: test/transaction_test.dart: transaction control postgres shorter statement limit retains its error and rolls back the transaction -03:29 +349: test/transaction_test.dart: transaction control postgres monotonic deadline prevents commit after a CPU-bound callback -03:29 +350: test/transaction_test.dart: transaction control postgres completed scopes remove their timers and cancellation listeners -03:29 +351: test/transaction_test.dart: transaction control postgres confirmed commit wins a cancellation race; lost acknowledgement stays unknown -03:29 +352: test/transaction_test.dart: transaction control postgres deferred constraint rejection at COMMIT is a known failure -03:29 +353: test/transaction_test.dart: transaction control postgres invalid deadlines and already cancelled tokens never begin a transaction -03:29 +354: test/transaction_test.dart: transaction control postgres SQLSTATE 40003 at COMMIT is never classified as a confirmed rejection -03:29 +355: test/transaction_test.dart: transaction control postgres real PostgreSQL serialization failure is classified after rollback -03:29 +356: test/transaction_test.dart: transaction control postgres real PostgreSQL deadlock rolls back exactly one transaction -03:30 +357: test/transaction_test.dart: SQLite COMMIT busy is a known rejection followed by verified rollback -03:31 +358: test/transaction_test.dart: SQLite deadline while BEGIN waits for a writer preserves its connection -03:31 +359: loading test/backfill_keys_test.dart -03:31 +359: test/backfill_keys_test.dart: backfill keys sqlite integer cursors preserve native values across resume -03:31 +360: test/backfill_keys_test.dart: backfill keys sqlite bigint cursors preserve native values across resume -03:31 +361: test/backfill_keys_test.dart: backfill keys sqlite text cursors preserve native values across resume -03:31 +362: test/backfill_keys_test.dart: backfill keys sqlite real cursors preserve native values across resume -03:31 +363: test/backfill_keys_test.dart: backfill keys sqlite boolean cursors preserve native values across resume -03:31 +364: test/backfill_keys_test.dart: backfill keys sqlite timestamp cursors preserve native values across resume -03:31 +365: test/backfill_keys_test.dart: backfill keys sqlite blob cursors preserve native values across resume -03:31 +366: test/backfill_keys_test.dart: backfill keys sqlite composite key batches account for every bound parameter -03:31 +367: test/backfill_keys_test.dart: backfill keys postgres integer cursors preserve native values across resume -03:31 +368: test/backfill_keys_test.dart: backfill keys postgres bigint cursors preserve native values across resume -03:31 +369: test/backfill_keys_test.dart: backfill keys postgres text cursors preserve native values across resume -03:31 +370: test/backfill_keys_test.dart: backfill keys postgres real cursors preserve native values across resume -03:31 +371: test/backfill_keys_test.dart: backfill keys postgres boolean cursors preserve native values across resume -03:31 +372: test/backfill_keys_test.dart: backfill keys postgres timestamp cursors preserve native values across resume -03:32 +373: test/backfill_keys_test.dart: backfill keys postgres blob cursors preserve native values across resume -03:32 +374: test/backfill_keys_test.dart: backfill keys postgres composite key batches account for every bound parameter -03:32 +375: test/backfill_keys_test.dart: invalid key, assignments and completion declarations fail before execution -03:32 +376: loading test/retry_test.dart -03:32 +376: test/retry_test.dart: retry sqlite opt-in retries roll back all writes and use new closed-over views -03:32 +377: test/retry_test.dart: retry sqlite default is one attempt; an explicit attempt limit is exact -03:32 +378: test/retry_test.dart: retry sqlite application and constraint errors and caught statement failures do not replay -03:32 +379: test/retry_test.dart: retry sqlite unknown commit and unconfirmed rollback prohibit replay -03:32 +380: test/retry_test.dart: retry sqlite retry time budget bounds active SQL and does not retry cancellation -03:32 +381: test/retry_test.dart: retry sqlite deadline and cancellation interrupt backoff without another callback -03:32 +382: test/retry_test.dart: retry sqlite time budget includes acquisition and abandons late callbacks -03:32 +383: test/retry_test.dart: retry sqlite transaction timeout is shared across attempts and sessions recover after expiry -03:32 +384: test/retry_test.dart: retry sqlite subscriptions publish only the final committed retry result -03:32 +385: test/retry_test.dart: retry sqlite invalid policies reject before SQL -03:32 +386: test/retry_test.dart: retry postgres opt-in retries roll back all writes and use new closed-over views -03:32 +387: test/retry_test.dart: retry postgres default is one attempt; an explicit attempt limit is exact -03:32 +388: test/retry_test.dart: retry postgres application and constraint errors and caught statement failures do not replay -03:32 +389: test/retry_test.dart: retry postgres unknown commit and unconfirmed rollback prohibit replay -03:32 +390: test/retry_test.dart: retry postgres retry time budget bounds active SQL and does not retry cancellation -03:32 +391: test/retry_test.dart: retry postgres deadline and cancellation interrupt backoff without another callback -03:33 +392: test/retry_test.dart: retry postgres time budget includes acquisition and abandons late callbacks -03:33 +393: test/retry_test.dart: retry postgres transaction timeout is shared across attempts and sessions recover after expiry -03:33 +394: test/retry_test.dart: retry postgres subscriptions publish only the final committed retry result -03:33 +395: test/retry_test.dart: retry postgres invalid policies reject before SQL -03:33 +396: test/retry_test.dart: retry postgres real unknown COMMIT response prohibits opted-in retry -03:33 +397: test/retry_test.dart: retry postgres real serialization conflict repeats the reads and decisions -03:33 +398: test/retry_test.dart: retry postgres real deadlock retries only the aborted callback -03:34 +399: test/retry_test.dart: SQLite WAL snapshot conflict replays reads after rollback -03:34 +400: test/retry_test.dart: SQLite real busy COMMIT: success -03:34 +401: test/retry_test.dart: SQLite real busy COMMIT: exhausted -03:34 +402: test/retry_test.dart: SQLite real busy COMMIT: deadline -03:34 +403: test/retry_test.dart: SQLite real busy COMMIT: cancel -03:34 +404: test/retry_test.dart: SQLite real busy COMMIT: shared-budget -03:34 +405: loading test/builder_test.dart -03:34 +405: test/builder_test.dart: build assets and standalone CLI produce identical client and snapshot -03:37 +406: test/builder_test.dart: multiple explicit schema roots resolve shared public types under lib -03:38 +407: test/builder_test.dart: invalid semantic_error never publishes partial outputs -03:39 +408: test/builder_test.dart: invalid enum_labels never publishes partial outputs -03:39 +409: test/builder_test.dart: invalid no_models never publishes partial outputs -03:40 +410: test/builder_test.dart: invalid part_file never publishes partial outputs -03:41 +411: test/builder_test.dart: unknown options fail instead of silently changing build behavior -03:41 +412: test/builder_test.dart: schema resolution can consume an earlier builder output -03:42 +413: loading test/temporal_precision_test.dart -03:42 +413: test/temporal_precision_test.dart: temporal precision sqlite generated writes, defaults and computed columns use database precision -03:42 +414: test/temporal_precision_test.dart: temporal precision sqlite SQL casts match all precisions on both sides of the PostgreSQL epoch -03:42 +415: test/temporal_precision_test.dart: temporal precision sqlite nulls, CTE references, grouping, streams and wrong scopes retain type semantics -03:42 +416: test/temporal_precision_test.dart: temporal precision sqlite rounded keys work in batch writes, upserts and foreign-key relation loading -03:42 +417: test/temporal_precision_test.dart: temporal precision sqlite catalog import preserves precision, defaults and computed expressions -03:44 +418: test/temporal_precision_test.dart: temporal precision sqlite narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -03:44 +419: test/temporal_precision_test.dart: temporal precision sqlite timestamp defaults and explicit six-digit catalogs retain precision -03:44 +420: test/temporal_precision_test.dart: temporal precision sqlite extended timestamps round in SQL without total-microsecond overflow -03:44 +421: test/temporal_precision_test.dart: temporal precision sqlite raw SQL respects native or managed precision constraints -03:44 +422: test/temporal_precision_test.dart: temporal precision postgres generated writes, defaults and computed columns use database precision -03:44 +423: test/temporal_precision_test.dart: temporal precision postgres SQL casts match all precisions on both sides of the PostgreSQL epoch -03:44 +424: test/temporal_precision_test.dart: temporal precision postgres nulls, CTE references, grouping, streams and wrong scopes retain type semantics -03:44 +425: test/temporal_precision_test.dart: temporal precision postgres rounded keys work in batch writes, upserts and foreign-key relation loading -03:44 +426: test/temporal_precision_test.dart: temporal precision postgres catalog import preserves precision, defaults and computed expressions -03:45 +427: test/temporal_precision_test.dart: temporal precision postgres narrowing precision requires a reviewed conversion and duplicate-key failure rolls back history -03:45 +428: test/temporal_precision_test.dart: temporal precision postgres timestamp defaults and explicit six-digit catalogs retain precision -03:45 +429: test/temporal_precision_test.dart: temporal precision postgres extended timestamps round in SQL without total-microsecond overflow -03:45 +430: test/temporal_precision_test.dart: temporal precision postgres raw SQL respects native or managed precision constraints -03:45 +431: test/temporal_precision_test.dart: value rounding keeps extended ranges and rejects overflow/invalid precision -03:45 +432: test/temporal_precision_test.dart: default and explicit six preserve historical snapshots and migration checksums -03:45 +433: test/temporal_precision_test.dart: generator rejects invalid annotations and preserves generated fixture metadata -03:47 +434: loading test/observation_test.dart -03:47 +434: test/observation_test.dart: observation sqlite actual lease queue and synchronous mapping have separate clocks -03:47 +435: test/observation_test.dart: observation sqlite expired acquisition emits one failure and no late SQL -03:47 +436: test/observation_test.dart: observation sqlite cancelled acquisition emits one failure and no late SQL -03:47 +437: test/observation_test.dart: observation sqlite sessions transactions and savepoints inherit hooks and report reused leases -03:47 +438: test/observation_test.dart: observation sqlite nested reads attribute decoding to each statement without repeating SQL -03:47 +439: test/observation_test.dart: observation sqlite SQL and mapper failures belong to their own phases -03:47 +440: test/observation_test.dart: observation sqlite cursor batches and returning writes produce decoding observations -03:47 +441: test/observation_test.dart: observation sqlite observer exceptions preserve successful commits and original mapper errors -03:47 +442: test/observation_test.dart: observation postgres actual lease queue and synchronous mapping have separate clocks -03:47 +443: test/observation_test.dart: observation postgres expired acquisition emits one failure and no late SQL -03:47 +444: test/observation_test.dart: observation postgres cancelled acquisition emits one failure and no late SQL -03:47 +445: test/observation_test.dart: observation postgres sessions transactions and savepoints inherit hooks and report reused leases -03:47 +446: test/observation_test.dart: observation postgres nested reads attribute decoding to each statement without repeating SQL -03:47 +447: test/observation_test.dart: observation postgres SQL and mapper failures belong to their own phases -03:48 +448: test/observation_test.dart: observation postgres cursor batches and returning writes produce decoding observations -03:48 +449: test/observation_test.dart: observation postgres observer exceptions preserve successful commits and original mapper errors -03:48 +450: test/observation_test.dart: synchronous driver acquisition failures are reported once -03:48 +451: loading test/integer_test.dart -03:48 +451: test/integer_test.dart: integer widths sqlite storage bounds, nulls and wider sums keep the normal integer value codec -03:48 +452: test/integer_test.dart: integer widths sqlite relations, union and cursors share int expressions across physical widths -03:48 +453: test/integer_test.dart: integer widths sqlite defaulted and quoted columns retain enforced nullable bounds -03:48 +454: test/integer_test.dart: integer widths sqlite Dart snapshots and imports retain widths without confusing value codecs -03:50 +455: test/integer_test.dart: integer widths sqlite widening and narrowing use reviewed conversions with atomic failure recovery -03:50 +456: test/integer_test.dart: integer widths sqlite column renames preserve width metadata and range constraints -03:50 +457: test/integer_test.dart: integer widths sqlite historical bounded backfills verify widths and resume normally -03:50 +458: test/integer_test.dart: integer widths sqlite changing an identity width preserves generation and rejects out-of-range explicit IDs -03:50 +459: test/integer_test.dart: integer widths sqlite quoted defaults and comments cannot masquerade as managed range checks -03:50 +460: test/integer_test.dart: integer widths sqlite extra weaker expressions remain unmanaged and cannot claim a narrower width -03:50 +461: test/integer_test.dart: integer widths postgres storage bounds, nulls and wider sums keep the normal integer value codec -03:50 +462: test/integer_test.dart: integer widths postgres relations, union and cursors share int expressions across physical widths -03:50 +463: test/integer_test.dart: integer widths postgres defaulted and quoted columns retain enforced nullable bounds -03:50 +464: test/integer_test.dart: integer widths postgres Dart snapshots and imports retain widths without confusing value codecs -03:50 +465: test/integer_test.dart: integer widths postgres widening and narrowing use reviewed conversions with atomic failure recovery -03:51 +466: test/integer_test.dart: integer widths postgres column renames preserve width metadata and range constraints -03:51 +467: test/integer_test.dart: integer widths postgres historical bounded backfills verify widths and resume normally -03:51 +468: test/integer_test.dart: integer widths postgres changing an identity width preserves generation and rejects out-of-range explicit IDs -03:51 +469: test/integer_test.dart: analyzer validates width annotations and integer-backed domain codecs -03:52 +470: test/integer_test.dart: width declarations reject wrong kinds and ranges without changing default snapshots -03:52 +471: loading test/relation_test.dart -03:52 +471: test/relation_test.dart: sqlite automatic unique joins preserve root pagination in one statement -03:52 +472: test/relation_test.dart: sqlite all-null projections still distinguish a present row from absence -03:52 +473: test/relation_test.dart: sqlite explicit batch deduplicates composite keys and skips partial nulls -03:52 +474: test/relation_test.dart: sqlite composite chunks include filter and window parameters with nested joins -03:52 +475: test/relation_test.dart: sqlite nested self joins and two edges to the same table use distinct aliases -03:52 +476: test/relation_test.dart: sqlite batch collection below a joined optional parent loads on the same plan -03:52 +477: test/relation_test.dart: sqlite join refuses unproven cardinality while automatic batch checks it -03:52 +478: test/relation_test.dart: sqlite filters and to-one pagination do not drop root rows -03:52 +479: test/relation_test.dart: sqlite joined records survive CTE projection and cursor decoding -03:52 +480: test/relation_test.dart: sqlite required tests row presence independently of nullable selected values -03:52 +481: test/relation_test.dart: sqlite relation aggregates restore the alias used by a joined selection -03:52 +482: test/relation_test.dart: sqlite reusing an identical edge deduplicates its join -03:52 +483: test/relation_test.dart: sqlite different filters need fresh relation occurrences -03:52 +484: test/relation_test.dart: sqlite large composite batches avoid expression-depth failure -03:52 +485: test/relation_test.dart: postgres automatic unique joins preserve root pagination in one statement -03:52 +486: test/relation_test.dart: postgres all-null projections still distinguish a present row from absence -03:52 +487: test/relation_test.dart: postgres explicit batch deduplicates composite keys and skips partial nulls -03:52 +488: test/relation_test.dart: postgres composite chunks include filter and window parameters with nested joins -03:52 +489: test/relation_test.dart: postgres nested self joins and two edges to the same table use distinct aliases -03:52 +490: test/relation_test.dart: postgres batch collection below a joined optional parent loads on the same plan -03:52 +491: test/relation_test.dart: postgres join refuses unproven cardinality while automatic batch checks it -03:52 +492: test/relation_test.dart: postgres filters and to-one pagination do not drop root rows -03:53 +493: test/relation_test.dart: postgres joined records survive CTE projection and cursor decoding -03:53 +494: test/relation_test.dart: postgres required tests row presence independently of nullable selected values -03:53 +495: test/relation_test.dart: postgres relation aggregates restore the alias used by a joined selection -03:53 +496: test/relation_test.dart: postgres reusing an identical edge deduplicates its join -03:53 +497: test/relation_test.dart: postgres different filters need fresh relation occurrences -03:53 +498: test/relation_test.dart: postgres large composite batches avoid expression-depth failure -03:53 +499: loading test/schema_boundary_test.dart -03:53 +499: test/schema_boundary_test.dart: schema rejects malformed names, keys and local FK definitions offline -03:53 +500: test/schema_boundary_test.dart: identifier case is resolved for the selected database -03:53 +501: test/schema_boundary_test.dart: generated sqlite schema executes its native computed-key contract -03:55 +502: test/schema_boundary_test.dart: generated postgres schema executes its native computed-key contract -03:56 +503: loading test/generated_database_test.dart -03:56 +503: test/generated_database_test.dart: (setUpAll) -03:56 +503: test/generated_database_test.dart: generated sqlite generated client, DDL, relations, dates and patches work together -03:56 +504: test/generated_database_test.dart: generated sqlite planning is read-only and repeat migration is a no-op -03:56 +505: test/generated_database_test.dart: generated sqlite compiled physical snapshot and full managed catalog comparison -03:56 +506: test/generated_database_test.dart: generated sqlite baseline preserves existing rows and verifies keys before recording history -03:56 +507: test/generated_database_test.dart: generated sqlite full verification sees defaults and additional unique constraints -03:56 +508: test/generated_database_test.dart: generated sqlite history tampering and missing applied migrations fail -03:56 +509: test/generated_database_test.dart: generated sqlite failed DDL rolls back schema and history atomically -03:56 +510: test/generated_database_test.dart: generated sqlite actual column drift is detected independently of history -03:56 +511: test/generated_database_test.dart: generated sqlite transaction control cannot be hidden behind SQL comments -03:56 +512: test/generated_database_test.dart: generated sqlite concurrent migration callers serialize against shared history -03:56 +513: test/generated_database_test.dart: generated postgres generated client, DDL, relations, dates and patches work together -03:56 +514: test/generated_database_test.dart: generated postgres planning is read-only and repeat migration is a no-op -03:56 +515: test/generated_database_test.dart: generated postgres compiled physical snapshot and full managed catalog comparison -03:56 +516: test/generated_database_test.dart: generated postgres baseline preserves existing rows and verifies keys before recording history -03:56 +517: test/generated_database_test.dart: generated postgres full verification sees defaults and additional unique constraints -03:56 +518: test/generated_database_test.dart: generated postgres history tampering and missing applied migrations fail -03:56 +519: test/generated_database_test.dart: generated postgres failed DDL rolls back schema and history atomically -03:56 +520: test/generated_database_test.dart: generated postgres actual column drift is detected independently of history -03:56 +521: test/generated_database_test.dart: generated postgres transaction control cannot be hidden behind SQL comments -03:56 +522: test/generated_database_test.dart: generated postgres concurrent migration callers serialize against shared history -03:56 +523: test/generated_database_test.dart: (tearDownAll) -03:56 +523: loading test/import_test.dart -03:57 +523: test/import_test.dart: catalog import sqlite imports records, identities, defaults and constraints then baselines unchanged rows -04:00 +524: test/import_test.dart: catalog import sqlite normalizes reserved, colliding and hostile identifiers without changing physical names -04:01 +525: test/import_test.dart: catalog import sqlite composite and self relations preserve key order and unique indexes -04:02 +526: test/import_test.dart: catalog import sqlite unsupported storage excludes its table while computed SQL imports with a review note -04:02 +527: test/import_test.dart: catalog import sqlite read-only snapshot and selected scope reject temporary shadowing and active transactions -04:02 +528: test/import_test.dart: catalog import sqlite physical storage mapping does not infer domain types from data -04:03 +529: test/import_test.dart: catalog import sqlite views triggers and specialized indexes remain in the review report -04:03 +530: test/import_test.dart: catalog import sqlite nullable primary keys, virtual tables and their storage tables need explicit handling -04:03 +531: test/import_test.dart: catalog import sqlite foreign keys with coercible but different Dart types are reported -04:04 +532: test/import_test.dart: catalog import postgres imports records, identities, defaults and constraints then baselines unchanged rows -04:05 +533: test/import_test.dart: catalog import postgres normalizes reserved, colliding and hostile identifiers without changing physical names -04:06 +534: test/import_test.dart: catalog import postgres composite and self relations preserve key order and unique indexes -04:06 +535: test/import_test.dart: catalog import postgres unsupported storage excludes its table while computed SQL imports with a review note -04:07 +536: test/import_test.dart: catalog import postgres read-only snapshot and selected scope reject temporary shadowing and active transactions -04:07 +537: test/import_test.dart: catalog import postgres physical storage mapping does not infer domain types from data -04:07 +538: test/import_test.dart: catalog import postgres views triggers and specialized indexes remain in the review report -04:07 +539: test/import_test.dart: catalog import postgres policy reports retain roles, commands, mode and row-security state -04:07 +540: test/import_test.dart: catalog import postgres always identity and inherited tables are not flattened into ordinary writable models -04:08 +541: loading test/database_test.dart -04:08 +541: test/database_test.dart: sqlite typed CRUD, exact projection and parameter binding -04:08 +542: test/database_test.dart: sqlite record and DTO mapping run only when decoding -04:08 +543: test/database_test.dart: sqlite immutable filters, empty IN, null and pagination -04:08 +544: test/database_test.dart: sqlite cross-table identity and duplicate assignments rejected before IO -04:08 +545: test/database_test.dart: sqlite commit, rollback, savepoint recovery and escaped session -04:08 +546: test/database_test.dart: sqlite caught statement failures cannot masquerade as a commit -04:08 +547: test/database_test.dart: sqlite recoverable constraint errors stay within their savepoint -04:08 +548: test/database_test.dart: sqlite unawaited work is drained and rolled back -04:08 +549: test/database_test.dart: sqlite close waits for active operations and rejects further use -04:08 +550: test/database_test.dart: sqlite leased sessions retain one connection across transactions and expire -04:08 +551: test/database_test.dart: sqlite native upsert updates atomically using existing and incoming values -04:08 +552: test/database_test.dart: sqlite bulk inserts split at parameter limits and opt into returning -04:08 +553: test/database_test.dart: sqlite a failed later bulk chunk rolls back all previous chunks -04:08 +554: test/database_test.dart: sqlite bulk optional field shapes preserve defaults and explicit null -04:08 +555: test/database_test.dart: sqlite relations CTEs preserve SQL column identity through mapped projections -04:08 +556: test/database_test.dart: sqlite relations explicit aliases support multiple joins and self joins -04:08 +557: test/database_test.dart: sqlite relations left join distinguishes a missing row from an all-null selection -04:08 +558: test/database_test.dart: sqlite relations correlated scalar, IN and EXISTS use nested SQL scopes -04:08 +559: test/database_test.dart: sqlite relations grouping is checked and window functions preserve row cardinality -04:08 +560: test/database_test.dart: sqlite relations root pagination and each parent limit use two queries -04:08 +561: test/database_test.dart: sqlite relations empty roots skip child queries; empty lists remain typed lists -04:08 +562: test/database_test.dart: sqlite relations nested relation projections and optional all-null projection -04:08 +563: test/database_test.dart: sqlite relations correlated any, none, count and every with SQL nulls -04:08 +564: test/database_test.dart: sqlite relations relation parameters bind correctly in projection and filter -04:08 +565: test/database_test.dart: sqlite keyset pagination uses a unique tie breaker in mixed directions -04:08 +566: test/database_test.dart: sqlite keyset pagination handles null order and validates versioned tokens -04:08 +567: test/database_test.dart: postgres typed CRUD, exact projection and parameter binding -04:08 +568: test/database_test.dart: postgres record and DTO mapping run only when decoding -04:08 +569: test/database_test.dart: postgres immutable filters, empty IN, null and pagination -04:08 +570: test/database_test.dart: postgres cross-table identity and duplicate assignments rejected before IO -04:08 +571: test/database_test.dart: postgres commit, rollback, savepoint recovery and escaped session -04:08 +572: test/database_test.dart: postgres caught statement failures cannot masquerade as a commit -04:08 +573: test/database_test.dart: postgres recoverable constraint errors stay within their savepoint -04:08 +574: test/database_test.dart: postgres unawaited work is drained and rolled back -04:08 +575: test/database_test.dart: postgres close waits for active operations and rejects further use -04:08 +576: test/database_test.dart: postgres leased sessions retain one connection across transactions and expire -04:08 +577: test/database_test.dart: postgres native upsert updates atomically using existing and incoming values -04:08 +578: test/database_test.dart: postgres bulk inserts split at parameter limits and opt into returning -04:08 +579: test/database_test.dart: postgres a failed later bulk chunk rolls back all previous chunks -04:08 +580: test/database_test.dart: postgres bulk optional field shapes preserve defaults and explicit null -04:08 +581: test/database_test.dart: postgres relations CTEs preserve SQL column identity through mapped projections -04:08 +582: test/database_test.dart: postgres relations explicit aliases support multiple joins and self joins -04:08 +583: test/database_test.dart: postgres relations left join distinguishes a missing row from an all-null selection -04:08 +584: test/database_test.dart: postgres relations correlated scalar, IN and EXISTS use nested SQL scopes -04:08 +585: test/database_test.dart: postgres relations grouping is checked and window functions preserve row cardinality -04:08 +586: test/database_test.dart: postgres relations root pagination and each parent limit use two queries -04:08 +587: test/database_test.dart: postgres relations empty roots skip child queries; empty lists remain typed lists -04:08 +588: test/database_test.dart: postgres relations nested relation projections and optional all-null projection -04:08 +589: test/database_test.dart: postgres relations correlated any, none, count and every with SQL nulls -04:08 +590: test/database_test.dart: postgres relations relation parameters bind correctly in projection and filter -04:08 +591: test/database_test.dart: postgres keyset pagination uses a unique tie breaker in mixed directions -04:08 +592: test/database_test.dart: postgres keyset pagination handles null order and validates versioned tokens -04:08 +593: loading test/union_test.dart -04:08 +593: test/union_test.dart: union sqlite SQL duplicate and NULL semantics use one statement -04:08 +594: test/union_test.dart: union sqlite heterogeneous records map only after SQL union and final pagination -04:08 +595: test/union_test.dart: union sqlite operand limits, order, parameters and final filter stay scoped -04:08 +596: test/union_test.dart: union sqlite repeated SQL expressions preserve positional columns -04:08 +597: test/union_test.dart: union sqlite nested sets preserve association, CTE exports and streaming -04:08 +598: test/union_test.dart: union sqlite same CTE names in distinct operands stay in separate scopes -04:08 +599: test/union_test.dart: union sqlite SQL scalar, IN subqueries, aggregate and zero-row results compose -04:08 +600: test/union_test.dart: union sqlite grouped branches and window branches remain SQL queries -04:08 +601: test/union_test.dart: union sqlite bound constant projections retain Dart types -04:08 +602: test/union_test.dart: union sqlite Dart maps and relation selections are rejected before execution -04:08 +603: test/union_test.dart: union sqlite codecs and nullability cannot silently change across operands -04:08 +604: test/union_test.dart: union sqlite shared domain codecs work, and nullable wrappers retain decoding semantics -04:08 +605: test/union_test.dart: union sqlite export references and outer join nullability are guarded -04:08 +606: test/union_test.dart: union sqlite compound mutations and mixed sessions fail before SQL -04:08 +607: test/union_test.dart: union sqlite all record arities use typed SQL decoding -04:08 +608: test/union_test.dart: union sqlite subscription observes committed changes in both operands -04:08 +609: test/union_test.dart: union postgres SQL duplicate and NULL semantics use one statement -04:08 +610: test/union_test.dart: union postgres heterogeneous records map only after SQL union and final pagination -04:08 +611: test/union_test.dart: union postgres operand limits, order, parameters and final filter stay scoped -04:08 +612: test/union_test.dart: union postgres repeated SQL expressions preserve positional columns -04:08 +613: test/union_test.dart: union postgres nested sets preserve association, CTE exports and streaming -04:08 +614: test/union_test.dart: union postgres same CTE names in distinct operands stay in separate scopes -04:09 +615: test/union_test.dart: union postgres SQL scalar, IN subqueries, aggregate and zero-row results compose -04:09 +616: test/union_test.dart: union postgres grouped branches and window branches remain SQL queries -04:09 +617: test/union_test.dart: union postgres bound constant projections retain Dart types -04:09 +618: test/union_test.dart: union postgres Dart maps and relation selections are rejected before execution -04:09 +619: test/union_test.dart: union postgres codecs and nullability cannot silently change across operands -04:09 +620: test/union_test.dart: union postgres shared domain codecs work, and nullable wrappers retain decoding semantics -04:09 +621: test/union_test.dart: union postgres export references and outer join nullability are guarded -04:09 +622: test/union_test.dart: union postgres compound mutations and mixed sessions fail before SQL -04:09 +623: test/union_test.dart: union postgres all record arities use typed SQL decoding -04:09 +624: test/union_test.dart: union postgres subscription observes committed changes in both operands -04:09 +625: loading test/import_cli_test.dart -04:09 +625: test/import_cli_test.dart: CLI sqlite imports reviews generates and baselines existing data without replacing files -04:51 +626: test/import_cli_test.dart: CLI postgres imports reviews generates and baselines existing data without replacing files -05:28 +627: loading test/decimal_division_test.dart -05:29 +627: test/decimal_division_test.dart: decimal division sqlite signed modes and scales match exact integer arithmetic -05:29 +628: test/decimal_division_test.dart: decimal division sqlite exact defaults reject lost digits and zero divisors, NULL propagates -05:29 +629: test/decimal_division_test.dart: decimal division sqlite precision exceeds native division scale without double rounding -05:29 +630: test/decimal_division_test.dart: decimal division sqlite huge remainders avoid overflowing intermediate scaled products -05:50 +631: test/decimal_division_test.dart: decimal division sqlite outermost integer scale and result overflow are explicit -05:51 +632: test/decimal_division_test.dart: decimal division sqlite aggregates windows CTEs predicates and unions compose -05:51 +633: test/decimal_division_test.dart: decimal division sqlite expression assignments and transaction rollback preserve values -05:51 +634: test/decimal_division_test.dart: decimal division sqlite window filtering ordering DISTINCT and pagination keep their SQL order -05:51 +635: test/decimal_division_test.dart: decimal division sqlite windows over grouped aggregates run after HAVING -05:51 +636: test/decimal_division_test.dart: decimal division sqlite window results compose through CTEs sets and correlated subqueries -05:51 +637: test/decimal_division_test.dart: decimal division sqlite batched relations retain per-parent window pagination -05:51 +638: test/decimal_division_test.dart: decimal division postgres signed modes and scales match exact integer arithmetic -05:51 +639: test/decimal_division_test.dart: decimal division postgres exact defaults reject lost digits and zero divisors, NULL propagates -05:51 +640: test/decimal_division_test.dart: decimal division postgres precision exceeds native division scale without double rounding -05:51 +641: test/decimal_division_test.dart: decimal division postgres huge remainders avoid overflowing intermediate scaled products -06:11 +642: test/decimal_division_test.dart: decimal division postgres outermost integer scale and result overflow are explicit -06:12 +643: test/decimal_division_test.dart: decimal division postgres aggregates windows CTEs predicates and unions compose -06:12 +644: test/decimal_division_test.dart: decimal division postgres expression assignments and transaction rollback preserve values -06:12 +645: test/decimal_division_test.dart: decimal division postgres window filtering ordering DISTINCT and pagination keep their SQL order -06:12 +646: test/decimal_division_test.dart: decimal division postgres windows over grouped aggregates run after HAVING -06:12 +647: test/decimal_division_test.dart: decimal division postgres window results compose through CTEs sets and correlated subqueries -06:12 +648: test/decimal_division_test.dart: decimal division postgres batched relations retain per-parent window pagination -06:12 +649: test/decimal_division_test.dart: decimal division postgres volatile operands are evaluated exactly once each -06:12 +650: loading test/query_boundary_test.dart -06:12 +650: test/query_boundary_test.dart: one failing cancellation listener cannot block other operations -06:12 +651: test/query_boundary_test.dart: sqlite explicit Backend construction rejects wrong transaction options before SQL -06:12 +652: test/query_boundary_test.dart: sqlite every close caller waits until the same pending lease is drained -06:12 +653: test/query_boundary_test.dart: sqlite subqueries and CTEs cannot escape their database or transaction -06:12 +654: test/query_boundary_test.dart: sqlite expired query cannot be embedded in its former parent -06:12 +655: test/query_boundary_test.dart: sqlite optional presence covers only its own left join -06:12 +656: test/query_boundary_test.dart: sqlite nested CTE decoding retains its nullable presence guards -06:12 +657: test/query_boundary_test.dart: sqlite mutation predicates and assignments reject aggregate/window SQL before execution -06:12 +658: test/query_boundary_test.dart: sqlite returning rejects empty selections and joined relationships -06:12 +659: test/query_boundary_test.dart: postgres explicit Backend construction rejects wrong transaction options before SQL -06:12 +660: test/query_boundary_test.dart: postgres every close caller waits until the same pending lease is drained -06:12 +661: test/query_boundary_test.dart: postgres subqueries and CTEs cannot escape their database or transaction -06:12 +662: test/query_boundary_test.dart: postgres expired query cannot be embedded in its former parent -06:12 +663: test/query_boundary_test.dart: postgres optional presence covers only its own left join -06:12 +664: test/query_boundary_test.dart: postgres nested CTE decoding retains its nullable presence guards -06:12 +665: test/query_boundary_test.dart: postgres mutation predicates and assignments reject aggregate/window SQL before execution -06:12 +666: test/query_boundary_test.dart: postgres returning rejects empty selections and joined relationships -06:12 +667: loading test/cli_test.dart -06:12 +667: test/cli_test.dart: Dart CLI generates, reviews, upgrades and verifies constraints without overwriting history -07:11 +668: test/cli_test.dart: Dart CLI baselines existing rows; read-only connections and invalid options reject writes -07:34 +669: test/cli_test.dart: Dart PostgreSQL configuration supports checked nontransactional recovery operations -08:12 +670: loading test/decimal_test.dart -08:12 +670: test/decimal_test.dart: exact decimal sqlite generated writes, defaults, patches and projections preserve all digits -08:12 +671: test/decimal_test.dart: exact decimal sqlite numeric ordering, predicates, IN, distinct and stable cursors -08:12 +672: test/decimal_test.dart: exact decimal sqlite sums, min/max, grouped aggregates and sliding windows stay exact -08:12 +673: test/decimal_test.dart: exact decimal sqlite computed CTEs, union results, subqueries and streaming keep decimal semantics -08:12 +674: test/decimal_test.dart: exact decimal sqlite numeric primary keys, foreign keys, joined and batched relations -08:12 +675: test/decimal_test.dart: exact decimal sqlite snapshots, catalog verification and imported declarations retain decimal storage -08:14 +676: test/decimal_test.dart: exact decimal sqlite aggregate intermediate overflow may cancel before a valid final result -08:14 +677: test/decimal_test.dart: exact decimal sqlite historical decimal keys resume backfills without lexical ordering or precision loss -08:14 +678: test/decimal_test.dart: exact decimal sqlite reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -08:14 +679: test/decimal_test.dart: exact decimal sqlite invalid external values fail decoding; later valid queries still work -08:14 +680: test/decimal_test.dart: exact decimal postgres generated writes, defaults, patches and projections preserve all digits -08:14 +681: test/decimal_test.dart: exact decimal postgres numeric ordering, predicates, IN, distinct and stable cursors -08:14 +682: test/decimal_test.dart: exact decimal postgres sums, min/max, grouped aggregates and sliding windows stay exact -08:14 +683: test/decimal_test.dart: exact decimal postgres computed CTEs, union results, subqueries and streaming keep decimal semantics -08:14 +684: test/decimal_test.dart: exact decimal postgres numeric primary keys, foreign keys, joined and batched relations -08:14 +685: test/decimal_test.dart: exact decimal postgres snapshots, catalog verification and imported declarations retain decimal storage -08:15 +686: test/decimal_test.dart: exact decimal postgres aggregate intermediate overflow may cancel before a valid final result -08:15 +687: test/decimal_test.dart: exact decimal postgres historical decimal keys resume backfills without lexical ordering or precision loss -08:15 +688: test/decimal_test.dart: exact decimal postgres reviewed text-to-decimal migration rejects duplicate numeric keys and rolls back -08:15 +689: test/decimal_test.dart: exact decimal postgres invalid external values fail decoding; later valid queries still work -08:15 +690: test/decimal_test.dart: decimal SQL rejects drivers without exact operations before execution -08:15 +691: test/decimal_test.dart: SQLite recognizes only column decimal collations, and detects drift -08:15 +692: loading test/watch_test.dart -08:15 +692: test/watch_test.dart: independent native SQLite workers require explicit notification -08:15 +693: test/watch_test.dart: watch sqlite lazy initial snapshot and relevant table invalidation -08:15 +694: test/watch_test.dart: watch sqlite transactions publish one snapshot after commit and none on rollback -08:15 +695: test/watch_test.dart: watch sqlite savepoint changes merge on release and disappear on rollback -08:15 +696: test/watch_test.dart: watch sqlite failed statements, zero-row writes and conflict no-ops do not refresh -08:15 +697: test/watch_test.dart: watch sqlite batch writes and upsert returning publish committed changes -08:15 +698: test/watch_test.dart: watch sqlite joined and nested batch relations track child writes -08:15 +699: test/watch_test.dart: watch sqlite relation predicates, SQL subqueries and CTE sources are dependencies -08:15 +700: test/watch_test.dart: watch sqlite declared cascade deletion invalidates a child-only query -08:15 +701: test/watch_test.dart: watch sqlite CASCADE tracks declared transitive deletion effects -08:15 +702: test/watch_test.dart: watch sqlite SET NULL tracks declared transitive deletion effects -08:15 +703: test/watch_test.dart: watch sqlite explicit JOINs and joined CTE definitions track both physical sources -08:15 +704: test/watch_test.dart: watch sqlite raw SQL declares changed and read tables; explicit invalidation follows transactions -08:15 +705: test/watch_test.dart: watch sqlite paused listeners coalesce writes and stop fetching until resumed -08:15 +706: test/watch_test.dart: watch sqlite invalidation during a read suppresses the stale snapshot and refetches -08:15 +707: test/watch_test.dart: watch sqlite decoding errors are reported and a later write can recover -08:15 +708: test/watch_test.dart: watch sqlite separate subscriptions and views of one driver receive changes -08:15 +709: test/watch_test.dart: watch sqlite watching leased sessions is rejected and cancelled tokens close the stream -08:15 +710: test/watch_test.dart: watch sqlite database close finishes watchers even when a listener is paused -08:15 +711: test/watch_test.dart: watch sqlite cancelling an active watched query interrupts SQL and releases its lease -08:15 +712: test/watch_test.dart: watch postgres lazy initial snapshot and relevant table invalidation -08:16 +713: test/watch_test.dart: watch postgres transactions publish one snapshot after commit and none on rollback -08:16 +714: test/watch_test.dart: watch postgres savepoint changes merge on release and disappear on rollback -08:16 +715: test/watch_test.dart: watch postgres failed statements, zero-row writes and conflict no-ops do not refresh -08:16 +716: test/watch_test.dart: watch postgres batch writes and upsert returning publish committed changes -08:16 +717: test/watch_test.dart: watch postgres joined and nested batch relations track child writes -08:16 +718: test/watch_test.dart: watch postgres relation predicates, SQL subqueries and CTE sources are dependencies -08:16 +719: test/watch_test.dart: watch postgres declared cascade deletion invalidates a child-only query -08:16 +720: test/watch_test.dart: watch postgres CASCADE tracks declared transitive deletion effects -08:16 +721: test/watch_test.dart: watch postgres SET NULL tracks declared transitive deletion effects -08:16 +722: test/watch_test.dart: watch postgres explicit JOINs and joined CTE definitions track both physical sources -08:16 +723: test/watch_test.dart: watch postgres raw SQL declares changed and read tables; explicit invalidation follows transactions -08:16 +724: test/watch_test.dart: watch postgres paused listeners coalesce writes and stop fetching until resumed -08:16 +725: test/watch_test.dart: watch postgres invalidation during a read suppresses the stale snapshot and refetches -08:16 +726: test/watch_test.dart: watch postgres decoding errors are reported and a later write can recover -08:16 +727: test/watch_test.dart: watch postgres separate subscriptions and views of one driver receive changes -08:16 +728: test/watch_test.dart: watch postgres watching leased sessions is rejected and cancelled tokens close the stream -08:16 +729: test/watch_test.dart: watch postgres database close finishes watchers even when a listener is paused -08:16 +730: test/watch_test.dart: watch postgres cancelling an active watched query interrupts SQL and releases its lease -08:16 +731: test/watch_test.dart: watch postgres lost commit acknowledgement triggers a conservative re-read -08:16 +732: test/watch_test.dart: watch postgres independent connections require an explicit external-write notification -08:16 +733: loading test/backfill_test.dart -08:16 +733: test/backfill_test.dart: backfill sqlite chunks pause and resume from a checksummed historical declaration -08:16 +734: test/backfill_test.dart: backfill sqlite failed chunk rolls back both writes and cursor, then resumes after repair -08:16 +735: test/backfill_test.dart: backfill sqlite failed completion proof preserves finished chunks without replay -08:16 +736: test/backfill_test.dart: backfill sqlite frozen upper bound and completion proof detect incompatible new writes -08:16 +737: test/backfill_test.dart: backfill sqlite competing runners share committed chunks without duplicate updates -08:16 +738: test/backfill_test.dart: backfill sqlite lost chunk commit acknowledgement preserves the committed frontier -08:16 +739: test/backfill_test.dart: backfill sqlite old checkpoint tables upgrade on apply; completed reads remain nonmutating -08:16 +740: test/backfill_test.dart: backfill sqlite completion conditions reject numeric results instead of truthiness -08:16 +741: test/backfill_test.dart: backfill sqlite empty input still requires the completion condition -08:16 +742: test/backfill_test.dart: backfill sqlite later ordinary migrations retain group atomicity after a completed backfill -08:16 +743: test/backfill_test.dart: backfill sqlite AFTER triggers cannot silently move keys past a committed cursor -08:16 +744: test/backfill_test.dart: backfill sqlite corrupt checkpoint dimensions and invalid run limits are rejected -08:16 +745: test/backfill_test.dart: backfill sqlite rebuild, chunked backfill and required-column rebuild preserve FK enforcement -08:16 +746: test/backfill_test.dart: backfill sqlite CLI previews non-atomic work and reports a bounded run as incomplete -08:35 +747: test/backfill_test.dart: backfill sqlite temporary tables cannot redirect the historical backfill target -08:36 +748: test/backfill_test.dart: backfill sqlite temporary objects cannot impersonate durable migration metadata -08:36 +749: test/backfill_test.dart: backfill sqlite committed chunks invalidate relevant typed query subscriptions -08:36 +750: test/backfill_test.dart: backfill sqlite real process exit after second chunk write resumes correctly -08:38 +751: test/backfill_test.dart: backfill sqlite real process exit after second chunk commit resumes correctly -08:41 +752: test/backfill_test.dart: backfill postgres chunks pause and resume from a checksummed historical declaration -08:41 +753: test/backfill_test.dart: backfill postgres failed chunk rolls back both writes and cursor, then resumes after repair -08:41 +754: test/backfill_test.dart: backfill postgres failed completion proof preserves finished chunks without replay -08:41 +755: test/backfill_test.dart: backfill postgres frozen upper bound and completion proof detect incompatible new writes -08:41 +756: test/backfill_test.dart: backfill postgres competing runners share committed chunks without duplicate updates -08:41 +757: test/backfill_test.dart: backfill postgres lost chunk commit acknowledgement preserves the committed frontier -08:41 +758: test/backfill_test.dart: backfill postgres old checkpoint tables upgrade on apply; completed reads remain nonmutating -08:41 +759: test/backfill_test.dart: backfill postgres completion conditions reject numeric results instead of truthiness -08:41 +760: test/backfill_test.dart: backfill postgres empty input still requires the completion condition -08:41 +761: test/backfill_test.dart: backfill postgres later ordinary migrations retain group atomicity after a completed backfill -08:41 +762: test/backfill_test.dart: backfill postgres AFTER triggers cannot silently move keys past a committed cursor -08:41 +763: test/backfill_test.dart: backfill postgres corrupt checkpoint dimensions and invalid run limits are rejected -08:41 +764: test/backfill_test.dart: backfill postgres CLI previews non-atomic work and reports a bounded run as incomplete -08:59 +765: test/backfill_test.dart: backfill postgres temporary tables cannot redirect the historical backfill target -08:59 +766: test/backfill_test.dart: backfill postgres temporary objects cannot impersonate durable migration metadata -08:59 +767: test/backfill_test.dart: backfill postgres row security cannot hide unfinished rows from the completion proof -08:59 +768: test/backfill_test.dart: backfill postgres inherited duplicate primary keys cannot advance a cursor -08:59 +769: test/backfill_test.dart: backfill postgres committed chunks invalidate relevant typed query subscriptions -08:59 +770: test/backfill_test.dart: backfill postgres real process exit after second chunk write resumes correctly -09:02 +771: test/backfill_test.dart: backfill postgres real process exit after second chunk commit resumes correctly -09:04 +772: loading test/custom_codec_test.dart -09:04 +772: test/custom_codec_test.dart: (setUpAll) -09:04 +772: test/custom_codec_test.dart: custom codecs sqlite generated custom values round trip through create, select and patch -09:04 +773: test/custom_codec_test.dart: custom codecs sqlite enum labels are stable text and unknown labels fail decoding -09:04 +774: test/custom_codec_test.dart: custom codecs sqlite domain validation and bound parameters apply to writes and predicates -09:04 +775: test/custom_codec_test.dart: custom codecs sqlite JSON documents distinguish SQL NULL, JSON null and string scalars -09:04 +776: test/custom_codec_test.dart: custom codecs sqlite nullable structured codec rejects JSON null instead of masking invalid data -09:04 +777: test/custom_codec_test.dart: custom codecs sqlite custom IDs and domain projections work across joined and batched relations -09:04 +778: test/custom_codec_test.dart: custom codecs sqlite custom values participate in atomic batch writes and native upsert -09:04 +779: test/custom_codec_test.dart: custom codecs sqlite custom IDs and enums survive cursor transport and streaming decoders -09:04 +780: test/custom_codec_test.dart: custom codecs sqlite migration snapshots retain physical storage without executing domain codecs -09:04 +781: test/custom_codec_test.dart: custom codecs postgres generated custom values round trip through create, select and patch -09:04 +782: test/custom_codec_test.dart: custom codecs postgres enum labels are stable text and unknown labels fail decoding -09:04 +783: test/custom_codec_test.dart: custom codecs postgres domain validation and bound parameters apply to writes and predicates -09:04 +784: test/custom_codec_test.dart: custom codecs postgres JSON documents distinguish SQL NULL, JSON null and string scalars -09:04 +785: test/custom_codec_test.dart: custom codecs postgres nullable structured codec rejects JSON null instead of masking invalid data -09:04 +786: test/custom_codec_test.dart: custom codecs postgres custom IDs and domain projections work across joined and batched relations -09:04 +787: test/custom_codec_test.dart: custom codecs postgres custom values participate in atomic batch writes and native upsert -09:04 +788: test/custom_codec_test.dart: custom codecs postgres custom IDs and enums survive cursor transport and streaming decoders -09:04 +789: test/custom_codec_test.dart: custom codecs postgres migration snapshots retain physical storage without executing domain codecs -09:04 +790: test/custom_codec_test.dart: (tearDownAll) -09:04 +790: loading test/sqlite_options_test.dart -09:04 +790: test/sqlite_options_test.dart: persistent uses the explicit native path and retains committed data -09:05 +791: test/sqlite_options_test.dart: native persistent storage never guesses a path or falls back to memory -09:05 +792: test/sqlite_options_test.dart: all driver close callers wait for active work and shutdown -09:05 +793: loading test/plan_test.dart -09:05 +793: test/plan_test.dart: inspect sqlite reports composite key width and validates each potential batch -09:05 +794: test/plan_test.dart: inspect sqlite compiles nested plans without acquiring or decoding -09:05 +795: test/plan_test.dart: inspect sqlite distinguishes same-statement joins and skipped batches -09:05 +796: test/plan_test.dart: inspect sqlite retains CTE, UNION and raw SQL boundaries -09:05 +797: test/plan_test.dart: plan execution sqlite one-key SQL templates match executed SQL and internal slots -09:05 +798: test/plan_test.dart: inspect postgres reports composite key width and validates each potential batch -09:05 +799: test/plan_test.dart: inspect postgres compiles nested plans without acquiring or decoding -09:05 +800: test/plan_test.dart: inspect postgres distinguishes same-statement joins and skipped batches -09:05 +801: test/plan_test.dart: inspect postgres retains CTE, UNION and raw SQL boundaries -09:05 +802: test/plan_test.dart: plan execution postgres one-key SQL templates match executed SQL and internal slots -09:05 +803: loading test/check_test.dart -09:05 +803: test/check_test.dart: CHECK sqlite generated named and unnamed constraints enforce values and permit NULL -09:05 +804: test/check_test.dart: CHECK sqlite import preserves names and SQL then generates a matching snapshot -09:07 +805: test/check_test.dart: CHECK sqlite add, change and remove constraints validate old rows atomically -09:07 +806: test/check_test.dart: CHECK sqlite duplicate unnamed checks consume and drop exactly one catalog entry -09:07 +807: test/check_test.dart: CHECK sqlite explicit checked renames preserve rows, incoming references, indexes and views -09:07 +808: test/check_test.dart: CHECK sqlite verification preserves casts, precedence, literals and trailing comments -09:07 +809: test/check_test.dart: CHECK sqlite quoted Unicode identifiers retain SQLite case distinctions -09:07 +810: test/check_test.dart: CHECK sqlite undeclared CHECK is refused before a rebuild and remains enforced -09:07 +811: test/check_test.dart: CHECK postgres generated named and unnamed constraints enforce values and permit NULL -09:07 +812: test/check_test.dart: CHECK postgres import preserves names and SQL then generates a matching snapshot -09:08 +813: test/check_test.dart: CHECK postgres add, change and remove constraints validate old rows atomically -09:08 +814: test/check_test.dart: CHECK postgres duplicate unnamed checks consume and drop exactly one catalog entry -09:08 +815: test/check_test.dart: CHECK postgres explicit checked renames preserve rows, incoming references, indexes and views -09:08 +816: test/check_test.dart: CHECK postgres verification preserves casts, precedence, literals and trailing comments -09:08 +817: test/check_test.dart: CHECK postgres catalog excludes unvalidated and NO INHERIT constraints -09:08 +818: test/check_test.dart: CHECK postgres verification plans but does not execute volatile row expressions -09:08 +819: test/check_test.dart: dialect overrides only alter the affected backend and preserve old snapshot JSON -09:08 +820: loading test/temporal_test.dart -09:08 +820: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: false -09:08 +821: test/temporal_test.dart: borrowed PostgreSQL pool local calendar capability: true -09:08 +822: test/temporal_test.dart: Gregorian cycles, era boundaries and full finite date range -09:08 +823: test/temporal_test.dart: local time precision, day endpoint and timestamp arithmetic -09:08 +824: test/temporal_test.dart: PostgreSQL binary registry covers boundaries without DateTime -09:08 +825: test/temporal_test.dart: local temporal sqlite generated writes, defaults, null patches and typed parameters -09:08 +826: test/temporal_test.dart: local temporal sqlite finite extremes and microseconds survive native rows and streams -09:08 +827: test/temporal_test.dart: local temporal sqlite calendar ordering, min/max, grouping and stable date cursors -09:08 +828: test/temporal_test.dart: local temporal sqlite equivalent date keys drive batched relations and unique indexes -09:08 +829: test/temporal_test.dart: local temporal sqlite time and local timestamp comparisons retain calendar semantics -09:08 +830: test/temporal_test.dart: local temporal sqlite reviewed text-to-time migration rolls back duplicate clock values -09:08 +831: test/temporal_test.dart: local temporal sqlite invalid external dates fail typed reads without damaging the connection -09:08 +832: test/temporal_test.dart: local temporal sqlite snapshots, catalog import and regenerated defaults agree -09:10 +833: test/temporal_test.dart: local temporal sqlite historical date keys resume a bounded backfill in calendar order -09:10 +834: test/temporal_test.dart: local temporal postgres catalog retains time-truncating casts in expression defaults -09:10 +835: test/temporal_test.dart: local temporal postgres generated writes, defaults, null patches and typed parameters -09:10 +836: test/temporal_test.dart: local temporal postgres finite extremes and microseconds survive native rows and streams -09:10 +837: test/temporal_test.dart: local temporal postgres calendar ordering, min/max, grouping and stable date cursors -09:10 +838: test/temporal_test.dart: local temporal postgres equivalent date keys drive batched relations and unique indexes -09:10 +839: test/temporal_test.dart: local temporal postgres time and local timestamp comparisons retain calendar semantics -09:10 +840: test/temporal_test.dart: local temporal postgres reviewed text-to-time migration rolls back duplicate clock values -09:11 +841: test/temporal_test.dart: local temporal postgres invalid external dates fail typed reads without damaging the connection -09:11 +842: test/temporal_test.dart: local temporal postgres snapshots, catalog import and regenerated defaults agree -09:11 +843: test/temporal_test.dart: local temporal postgres historical date keys resume a bounded backfill in calendar order -09:11 +844: loading test/unconstrained_relation_test.dart -09:11 +844: test/unconstrained_relation_test.dart: unconstrained sqlite catalogs contain no FK, implicit index or uniqueness promise -09:11 +845: test/unconstrained_relation_test.dart: unconstrained sqlite adding and removing a real FK remains a reviewed physical migration -09:11 +846: test/unconstrained_relation_test.dart: unconstrained sqlite unique joins preserve root rows, NULL components and missing targets -09:11 +847: test/unconstrained_relation_test.dart: unconstrained sqlite required target and filter visibility remain explicit -09:11 +848: test/unconstrained_relation_test.dart: unconstrained sqlite nonunique composite collections batch by tuple and page per parent -09:11 +849: test/unconstrained_relation_test.dart: unconstrained sqlite to-one uses cardinality checks without assuming a unique lookup -09:11 +850: test/unconstrained_relation_test.dart: unconstrained sqlite correlated filters and counts do not materialize related rows -09:11 +851: test/unconstrained_relation_test.dart: unconstrained sqlite self relations and inverse collections share the existing planner -09:11 +852: test/unconstrained_relation_test.dart: unconstrained sqlite delete has no cascade and watches track the joined target -09:11 +853: test/unconstrained_relation_test.dart: unconstrained sqlite transaction reads observe writes and outer rollback restores targets -09:11 +854: test/unconstrained_relation_test.dart: unconstrained sqlite streaming preserves optional and batched relation result shapes -09:11 +855: test/unconstrained_relation_test.dart: unconstrained sqlite floating relation keys retain storage intent and key equality -09:11 +856: test/unconstrained_relation_test.dart: unconstrained postgres catalogs contain no FK, implicit index or uniqueness promise -09:12 +857: test/unconstrained_relation_test.dart: unconstrained postgres adding and removing a real FK remains a reviewed physical migration -09:12 +858: test/unconstrained_relation_test.dart: unconstrained postgres unique joins preserve root rows, NULL components and missing targets -09:12 +859: test/unconstrained_relation_test.dart: unconstrained postgres required target and filter visibility remain explicit -09:12 +860: test/unconstrained_relation_test.dart: unconstrained postgres nonunique composite collections batch by tuple and page per parent -09:12 +861: test/unconstrained_relation_test.dart: unconstrained postgres to-one uses cardinality checks without assuming a unique lookup -09:12 +862: test/unconstrained_relation_test.dart: unconstrained postgres correlated filters and counts do not materialize related rows -09:12 +863: test/unconstrained_relation_test.dart: unconstrained postgres self relations and inverse collections share the existing planner -09:12 +864: test/unconstrained_relation_test.dart: unconstrained postgres delete has no cascade and watches track the joined target -09:12 +865: test/unconstrained_relation_test.dart: unconstrained postgres transaction reads observe writes and outer rollback restores targets -09:12 +866: test/unconstrained_relation_test.dart: unconstrained postgres streaming preserves optional and batched relation result shapes -09:12 +867: test/unconstrained_relation_test.dart: unconstrained postgres floating relation keys retain storage intent and key equality -09:12 +868: loading test/decimal_value_test.dart -09:12 +868: test/decimal_value_test.dart: normalization, scientific notation, equality and hash are exact -09:12 +869: test/decimal_value_test.dart: invalid and approximate inputs fail instead of silently rounding -09:12 +870: test/decimal_value_test.dart: range boundaries are checked without binary numeric conversions -09:12 +871: test/decimal_value_test.dart: addition, subtraction, multiplication and ordering keep every digit -09:12 +872: test/decimal_value_test.dart: division requires an explicit scale and rejects implicit rounding -09:12 +873: test/decimal_value_test.dart: rounding modes handle negative values, ties and negative scales -09:12 +874: test/decimal_value_test.dart: small fixed-scale arithmetic agrees with independent integer arithmetic -09:12 +875: All tests passed! diff --git a/research/validation/sqlite-refactor-web-assets.log b/research/validation/sqlite-refactor-web-assets.log deleted file mode 100644 index 3e374046..00000000 --- a/research/validation/sqlite-refactor-web-assets.log +++ /dev/null @@ -1 +0,0 @@ -Running build hooks...Running build hooks...Copied SQLite browser assets to .dart_tool/sqlite-assets-export diff --git a/research/validation/sqlite-refactor.json b/research/validation/sqlite-refactor.json deleted file mode 100644 index 99f9f59f..00000000 --- a/research/validation/sqlite-refactor.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "date": "2026-09-16", - "handwrittenRuntimeSha256": "d2b6a0a87cac75d0157cb631ad90e699c3fdf13be3e51f9fce876615bda50c24", - "runtimeHashInputs": "Sorted lib and bin Dart sources excluding generated web_build.dart, plus pubspec.yaml and pubspec.lock; path NUL bytes NUL.", - "native": { - "passed": true, - "tests": 875, - "duration": "09:12", - "postgres": "18.4", - "finalFocusedTests": 5 - }, - "packagedAssets": [ - { - "path": "assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "sha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4", - "bytes": 748686 - }, - { - "path": "assets/sqlite/worker-8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845.js", - "sha256": "8230ef505870d53bc4e4ebfc3714d5b12b742e1351ccc5153d5d27c9d3045845", - "bytes": 176630 - } - ], - "flutter": { - "version": "3.47.4", - "dart": "3.13.3", - "webExampleWasmBuild": "passed", - "androidAssetBundle": "passed; no SQLite Web assets; not a device runtime test" - }, - "web": { - "chrome": "153", - "dartJs": 21, - "dartWasm": 21, - "flutterJs": 21, - "flutterWasmWithoutIsolation": 21, - "flutterWasmWithIsolation": 21 - }, - "analyze": "passed", - "cliWebAssetExport": "passed" -} diff --git a/research/validation/sqlite-refactor.md b/research/validation/sqlite-refactor.md deleted file mode 100644 index 880d80e6..00000000 --- a/research/validation/sqlite-refactor.md +++ /dev/null @@ -1,70 +0,0 @@ -# Unified SQLite refactor validation - -Captured 2026-09-16 on Dart 3.13.3, Flutter 3.47.4 and Chrome 153. The -[machine-readable summary](sqlite-refactor.json) records the handwritten runtime -fingerprint and final packaged asset hashes. - -The SQLite entrypoint now selects native or browser transport internally. Both -use the same sqlite3 statement configuration, execution and cursor implementation. -The public Web-only driver entry has been removed. Flutter consumes package-owned -assets by default; ordinary Dart Web apps export them with `orm web-assets`. -Resources have content-hash filenames, a startup protocol/build handshake and -WASM subresource integrity verification. - -| Validation | Result | -| --- | --- | -| Complete native suite, real SQLite and PostgreSQL 18.4 enabled | **875 passed in 09:12**, exit 0 | -| Final focused native persistence/close/export checks | **5 passed**, exit 0 | -| Plain Dart JS / Dart WASM in Chrome | **21 / 21 passed** | -| Flutter release JS, no isolation headers | **21 passed** | -| Flutter release WASM, no isolation headers | **21 passed**, actual WASM confirmed | -| Flutter release WASM, isolation headers | **21 passed**, actual WASM confirmed | -| Repository Flutter example, `build web --wasm` | Passed | -| Android arm64 debug asset bundle | Built; SQLite Web assets absent | -| CLI Web asset export and packaged fingerprint checks | Passed | -| Static analysis and whitespace diff check | Passed | - -The complete native run covers the shared executor and new persistent-path/close -behavior. Final packaging removed the unused worker source-map reference; focused -native checks and all Web configurations were then rerun against those artifacts. -The negative Web scenario separately rejects an old protocol, a different build -with the current protocol, a wrong WASM integrity value, and native file storage. - -Flutter tests consume unchanged package asset declarations and default connection -configuration. They start on `/nested/detail/42` with ``, -retain the Flutter widget tree, and assert animation ticks advance during a long -worker SQL query. The captured checks also verify typed relationships and -projections, transactions/savepoints, bounded cursors, commit notifications, -numeric/temporal precision, exclusive OPFS ownership, page reload with an open -transaction, data recovery and subsequent migrations. - -Evidence: - -- [Native full run](sqlite-refactor-native.log) and [focused checks](sqlite-refactor-focused.log). -- [Plain Dart JS](sqlite-refactor-browser-js.json) and [Dart WASM](sqlite-refactor-browser-wasm.json). -- [Three Flutter Web configurations](sqlite-refactor-flutter-web.json), including - actual compilation mode, isolation state and asset request paths. -- [Asset verification](sqlite-refactor-assets-check.log), [CLI export](sqlite-refactor-web-assets.log), - [analysis](sqlite-refactor-analyze.log) and [example build](sqlite-refactor-flutter-example.log). - -Reproduce from the repository root, with a cached/installed Flutter SDK: - -```sh -dart run tool/build_sqlite_web.dart --check -dart run tool/test_browser.dart -dart run tool/test_browser.dart --wasm -dart run tool/test_flutter_web.dart /absolute/path/to/flutter -dart run orm web-assets -ORM_TEST_POSTGRES=postgresql://postgres@127.0.0.1:65439/postgres dart test -``` - -On this host, `DEVELOPER_DIR=/Library/Developer/CommandLineTools` selects the -available command-line toolchain. Native-asset Dart commands were run sequentially. -The temporary PostgreSQL instance was stopped and removed; browser profiles and -local servers were disposed by the runners. - -The Android result establishes asset filtering, not APK installation or execution. -Android/Apple runtime, Safari/Firefox, Flutter hot restart with an open connection, -CDN/CSP hosting, multi-tab database sharing, offline upgrade deployment and -throughput remain outside this capture. The material-icon font warning in Flutter -release output does not fail compilation. No new performance claim is made. diff --git a/research/validation/standalone-browser.json b/research/validation/standalone-browser.json deleted file mode 100644 index 972d6afd..00000000 --- a/research/validation/standalone-browser.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "capturedAt": "2026-09-19T13:39:24.071851+00:00", - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "reports": [ - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 65 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 42 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 16 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 20 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 18 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 37 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 28 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 4 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 2 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 34 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 95 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 21 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "js", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 60 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 40 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 13 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 20 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 18 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 23 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 2 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 29 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 33 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 93 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 12 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "dartCompilation": "wasm", - "dartSdk": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"macos_arm64\"", - "sqlitePackage": "3.6.0", - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - } - ], - "limits": [ - "Chrome 153 on macOS only; other browsers and WebViews were not validated.", - "Dart JS and Dart WASM are distinct runtime executions, not Flutter builds." - ] -} diff --git a/research/validation/standalone-flutter-android.png b/research/validation/standalone-flutter-android.png deleted file mode 100644 index 5b9b4be0..00000000 Binary files a/research/validation/standalone-flutter-android.png and /dev/null differ diff --git a/research/validation/standalone-flutter-web.json b/research/validation/standalone-flutter-web.json deleted file mode 100644 index 1cce28d5..00000000 --- a/research/validation/standalone-flutter-web.json +++ /dev/null @@ -1,517 +0,0 @@ -{ - "capturedAt": "2026-09-19T13:39:24.071851+00:00", - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "reports": [ - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 92 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 40 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 15 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 20 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 18 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 32 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 27 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 2 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 1 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 3 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 160 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 35 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 100 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 2 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 20 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": false, - "flutterFrames": 5, - "baseUri": "http://127.0.0.1:64406/nested/", - "dartUriBase": "http://127.0.0.1:64406/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/favicon.ico", - "/nested/main.dart.js", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/chromium/canvaskit.wasm", - "/nested/canvaskit/chromium/canvaskit.js", - "/nested/main.dart.js", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/report" - ], - "build": "js", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 94 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 41 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 13 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 18 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 28 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 24 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 3 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 32 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 33 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 96 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 14 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 5, - "baseUri": "http://127.0.0.1:64426/nested/", - "dartUriBase": "http://127.0.0.1:64426/nested/detail/42?phase=recover", - "isolated": false, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/favicon.ico", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/report" - ], - "build": "wasm", - "expectedIsolation": false, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - }, - { - "passed": true, - "checks": [ - { - "name": "many-to-many plans, phase observations, payloads, pagination and transaction writes preserve query counts", - "milliseconds": 56 - }, - { - "name": "temporal column precision preserves epoch ties, extended ranges, defaults and keys", - "milliseconds": 38 - }, - { - "name": "memory migrations and verified foreign keys", - "milliseconds": 12 - }, - { - "name": "query ownership, optional guards and write validation survive JS and WASM", - "milliseconds": 19 - }, - { - "name": "client factories distinguish omission, explicit null and prepared batch values", - "milliseconds": 17 - }, - { - "name": "computed stored and virtual fields recompute across CRUD and migrations", - "milliseconds": 27 - }, - { - "name": "generated CHECK enforcement and atomic constraint migrations", - "milliseconds": 23 - }, - { - "name": "generated records, projections and typed relation batches", - "milliseconds": 2 - }, - { - "name": "rollback, savepoints and transaction lifetime", - "milliseconds": 1 - }, - { - "name": "stream cursor batches and early release", - "milliseconds": 1 - }, - { - "name": "commit invalidation and watch snapshots", - "milliseconds": 0 - }, - { - "name": "exact BigInt, Decimal, blobs and calendar transport", - "milliseconds": 2 - }, - { - "name": "explicit unsupported cancellation", - "milliseconds": 0 - }, - { - "name": "large real values retain floating storage", - "milliseconds": 2 - }, - { - "name": "instant microseconds are exact or explicitly rejected", - "milliseconds": 0 - }, - { - "name": "worker does not block the browser event loop", - "milliseconds": 158 - }, - { - "name": "startup failures are bounded and release failed workers", - "milliseconds": 30 - }, - { - "name": "unified entry rejects native paths and mismatched browser assets", - "milliseconds": 33 - }, - { - "name": "OPFS commit survives a closed worker and new connection", - "milliseconds": 95 - }, - { - "name": "page reload recovers committed rows and rolls back interrupted transaction", - "milliseconds": 1 - }, - { - "name": "persistent schema upgrades after reload preserve existing data", - "milliseconds": 13 - } - ], - "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36", - "actualWasm": true, - "flutterFrames": 4, - "baseUri": "http://127.0.0.1:64446/nested/", - "dartUriBase": "http://127.0.0.1:64446/nested/detail/42?phase=recover", - "isolated": true, - "requests": [ - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/favicon.ico", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/canvaskit/skwasm.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/missing.wasm", - "/missing-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/mismatched-worker.js", - "/mismatched-worker.js", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/detail/42", - "/nested/flutter_bootstrap.js", - "/nested/canvaskit/skwasm.wasm", - "/nested/canvaskit/skwasm.js", - "/nested/main.dart.mjs", - "/nested/main.dart.wasm", - "/nested/assets/FontManifest.json", - "/nested/assets/fonts/MaterialIcons-Regular.otf", - "/nested/assets/fonts/fallback/Roboto-Regular.ttf", - "/nested/assets/packages/orm/assets/sqlite/worker-cdda086086b0550d51668eb4ef26e21d27c54d769a16f5defdd45b3ec14043ce.js", - "/nested/assets/packages/orm/assets/sqlite/sqlite3-13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4.wasm", - "/nested/canvaskit/skwasm.js", - "/report" - ], - "build": "wasm", - "expectedIsolation": true, - "flutterSdk": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "repositoryUrl": "https://github.com/flutter/flutter.git", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "frameworkCommitDate": "2026-09-10 15:25:10 -0700", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "engineCommitDate": "2026-09-03 16:07:13.000Z", - "engineContentHash": "0e228ec8c8d2abc9fcf1d053e8a40665bb859ec7", - "engineBuildDate": "2026-09-04 00:39:13.722", - "dartSdkVersion": "3.13.3", - "devToolsVersion": "2.60.0", - "flutterVersion": "3.47.4" - }, - "sqliteWasmSha256": "13d3f11d05b39ba0618a7115fb41640a5d48b6300f5d3f325f554b42bd6688a4" - } - ], - "limits": [ - "Flutter release JS and WASM on Chrome 153; other browsers were not validated.", - "Animation frame progress is responsiveness evidence, not a frame-rate benchmark." - ] -} diff --git a/research/validation/standalone-flutter.json b/research/validation/standalone-flutter.json deleted file mode 100644 index 821e34f4..00000000 --- a/research/validation/standalone-flutter.json +++ /dev/null @@ -1,329 +0,0 @@ -{ - "capturedAt": "2026-09-19T12:55:23.887394Z", - "runtimeSourceCommit": "7a54553933ae1d768576a54a4234b00b6bfcf97e", - "flutter": { - "frameworkVersion": "3.47.4", - "channel": "stable", - "frameworkRevision": "9584c6713b324636289d067944a46fd6b49df14b", - "engineRevision": "06a2e2a110089dff50fe635cffd2a61e1b24fbcd", - "dartSdkVersion": "3.13.3" - }, - "device": "emulator-5582", - "abi": "arm64-v8a", - "buildFingerprint": "google/sdk_gphone64_arm64/emu64a:15/AE3A.240806.036/12592187:user/release-keys", - "apkSha256": { - "legacy": "87d5a37f8f4fc639f5b8e4609f6f900574eff032f594ea991fc29c5aae945705", - "current": "301e3abd15ce011dc659f4b16b114c3c15d722d30efd2340e1d6db417165aa6a" - }, - "sourceSha256": { - "lib/mariadb.dart": "2bae2c18bd9e5008e4899fd182ee6d73276e35169fdb2080d97ed212fcb42dc6", - "lib/builder.dart": "ca071eeeb6bc697c76518e973841722fb11822137c987d32aa0c2a04fb82802c", - "lib/drivers/mariadb.dart": "a3a61b50493ccba032badae1cf259824c68caf3c016aefc29fc996e92e53680a", - "lib/drivers/sqlite.dart": "9aecf50170c3731eacc71778f5c30185675c35bc5b3eb70ffece8ca96cd19d83", - "lib/drivers/postgres.dart": "9d9f30c2bba598c136e42c728c76ece32758d77bf32ccf2e6d437c3a8b2b7f7d", - "lib/drivers/mysql.dart": "5e5363a2bac9b680129d95ce2896eaee63dabd623abec373cd16ea7c51e7dd27", - "lib/values.dart": "db77f9169d78c2a19b8b13e1d066d94cf2a625188df6e2c42bc9065edb9af0be", - "lib/orm.dart": "606673c3c0fd0104663174979b456b2cbc15ca5e3aa55d74240adb5ccaa7ce21", - "lib/generate.dart": "db47ac06f250bc74bef1b670a2d114b99f0e6e5d01eb111a5e36d8887a102b2b", - "lib/sqlite_web_worker.dart": "0f4092c9528303f50f75f48c8d050e83461f795995af74b3fb97776724e9c19e", - "lib/migrate_cli.dart": "5b052713fc4769c7e29949a9c079738c771a31a6c7c52a6b4c7bfb30e4b3cf71", - "lib/sql.dart": "d86c7a6ff9165923edcf22ff6af5bc82c2d3d6a3086ba52417fc10b8fe952b3e", - "lib/sqlite.dart": "4447db13526e4b41f6d1e6057fb24b8d2f39299926ab02acfb6fc5210c8e6f8d", - "lib/schema.dart": "0e93f61e58ed1a8e6b8bb8f642ebcc63355351e4bb2121586443f0b7fb79fa0f", - "lib/postgres.dart": "97369ee6578e9f4ebd6bbd317dd4916b2f70df6d6f3475642adcaed3b1e31a86", - "lib/driver.dart": "b62e3ced5e0b8186e26c776e7c1d1bbea33b4334feb2d5e676733c9fd4e2fa53", - "lib/mysql.dart": "1dc98a260b895e0a025730e993b2128b44a379d5f4bc82f00416937e3e682a85", - "lib/migrate.dart": "cafddc611d9e1e16ad767d1d536f18810911898283d77a590f38c5754df264b0", - "lib/cli.dart": "d99abbee9ee2aacd81f7acbfa6b4808081fcb3298650acb5334d5db75474972e", - "lib/runtime.dart": "f70339493f0bed80babf5126d6560a804c0e6b67e533b3525a56fc81f79a1c9b", - "lib/schema_model.dart": "9271ae3bb44651caf687791b143dc8b594cd9ab22b1905b9e60ad8aea7113033", - "lib/src/instant.dart": "9b07f4a3daa73f5e5c1edb8b61ef0af6e18e55d72e263244e280b75fcf76d881", - "lib/src/temporal.dart": "f8e5d85edff8ac00a7f583e20f6bd28024e7e8513e77751e1d3a1d6d4413a3b6", - "lib/src/mutation.dart": "bff962d888cb753d2f50071772e4b98149432ac2620654488e100b4c53e2b7a6", - "lib/src/union.dart": "099e0845c027b0f6c017f69d1b5e182642729b3d39199c2e110cea72adb94ee2", - "lib/src/execution.dart": "abed3bbd3f34d17e148ed6c691a02aa308b17b1392b90a3a9bb72c172d3fc56c", - "lib/src/temporal_sql.dart": "fa0a4ea98197184d1c412c23448de46a2132970e590ad01a503dde4619fac7c4", - "lib/src/batch.dart": "aa25b00a337a86721511e7f11ac60934d153b2aec6340649c66f6d422cf69c91", - "lib/src/generate.dart": "cf86addb049826c963f4e6b79b45c7571839655836150b663c6f89cce493e67b", - "lib/src/runtime/mysql_transaction.dart": "958d991149e5a48f8542e0f55f1e167a510a90bded00690373d1c0c197a50d10", - "lib/src/runtime/session_connection.dart": "6d82dfe9eaafb1d5f6877f6e791987834666a98e162996bc07182aa8d344b74c", - "lib/src/runtime/database.dart": "7dbf07b4f6f2e27bed329fa1258ee3f3e49ff59714070926dcb1a3bb00bdafbb", - "lib/src/runtime/stream.dart": "3a0874d49e8761d55d194aadfafc7ccaf573eb8a593b317cd6f8a432545dc99e", - "lib/src/runtime/options.dart": "a6ccc7adcb63776fcc30d30acffc79618a002ddddb48a166963863a90b06549b", - "lib/src/sql.dart": "502e216814be3315c2532affd2a3fe501f9b121401ebc6873623970dd22d8aab", - "lib/src/watch.dart": "e7c03459b30dd1f84b2c589bf553696caf0dcaf942b2e1ee8ba15a638fca6cda", - "lib/src/selection.dart": "aa291734891a6fb5688369578dcc6ce199311e84789b7343f1003a1a2e02f7ac", - "lib/src/database.dart": "289c3fe1526767f986cc25d352bd9b6e8d1358e9fc388e31d91ac9d2fac1ec79", - "lib/src/decimal_average.dart": "5b29af955d294a0e7ed721ab124ce59072744ad22cb3d91fb00432a8c3ba3f0c", - "lib/src/sqlite/failure.dart": "eebf787d150938d8b3e66ce401e74ac364e963a0e04e2e1234f38cf99ed94b15", - "lib/src/sqlite/functions.dart": "cb451eb4cefc2d16e02b943ad59e41a1a0cbad794d7832755a4da7a38e1cd54c", - "lib/src/sqlite/temporal.dart": "0e874da5e0f6aa31d2d23db8edd458306d4e0dec2ce46397f471443186946243", - "lib/src/sqlite/web_build.dart": "59102e97a97ea829aa4cd9a3efdaf90bfe021ea63bacdeaf03f037adf0dddcc9", - "lib/src/sqlite/execution.dart": "05cffc662b52b0856e328d3c7b44d6fe7595770c0a3530a81200d6df14be13f9", - "lib/src/sqlite/web_wire.dart": "ecf8c03d3521ecc32a7449d6ef515be382e1bdd9e8d571d30914a93a1fb024b4", - "lib/src/sqlite/web_client.dart": "db62611ea09f0022ca6964d644ab2ea49e309c4530e581f7c44ede11af5a791c", - "lib/src/sqlite/native_client.dart": "aef0e510bdf6d2c9eaf4520f1f07e1f3a40db8f94abadb5f4f70e31b0bca8910", - "lib/src/sqlite/web.dart": "06eed380551685e4b06e3567533da2fb4d69c8025148d94d1b4ac29c5ff7cc3b", - "lib/src/sqlite/decimal.dart": "b291e5cfc0758fd944e7a09b345f8fa04b3e1679a099f92b9bf43023f57da5f0", - "lib/src/sqlite/native_server.dart": "24bdd930b4821cdf71129137826b05aea054426d96f3567bb2e682d1e8b89e40", - "lib/src/sqlite/native.dart": "94462da92cc021e5d2604031a2c3f4974c34ddd0a9a12a3d877ab60dc8e73a15", - "lib/src/sqlite/unsupported.dart": "f52c9b9709d54924856ca19e17a2d71bc39e0bfafefbb7967b496a721c5bc820", - "lib/src/sqlite/opened.dart": "1454c87229040cf6bc0a95c44bca27a98f791cf25154e7a91722842a1b2e80fd", - "lib/src/sqlite/assets_io.dart": "9ad78c5bb7b7de019ffe5a5b4454938e9d84bbbe9f52f171adecb738b2235912", - "lib/src/sqlite/options.dart": "32fa83feab3ea9543d749e1cfc9eefcccbc5af7d7b8477498ab0954132eba024", - "lib/src/cli/init.dart": "b9d9f8da97bc9f04e09498f26a6ec1a4076e1779d849cd2414a50d311c2e4f26", - "lib/src/cli/commands.dart": "1d0a3c84ef902a36cbb8543524ec2c91a278526c05819fc9788f784f71542044", - "lib/src/decimal_sql.dart": "c1d098a543f3e91064fc8f239b672f12d0523cbd66ff18a0277797271b0cf860", - "lib/src/table.dart": "e85d28ecdfdfa9335767403c26c657e6374a6a3d94dbb8c4057d4f3595435839", - "lib/src/decimal.dart": "87ab6b8a959d5b38bcff27099997ab420cd038cfe411c5c27348d5f299f7643f", - "lib/src/query.dart": "2f4f296796ad65f40540986fbb2c0a7cfbe7549193f4916830c6398ff56f1772", - "lib/src/sql_context.dart": "4cb07dd5038608a85fcadaf548cac6c9c063751bb59ff54ad55a7660ddebe8ba", - "lib/src/postgres/temporal.dart": "6a62d65c2fe5f5968c3c73c40ae456c1ebea44723fd9ba2f0b4a4adb850d33ba", - "lib/src/decimal_division.dart": "96c209cfe4ff9ef03c31709d0dacfa03a9f0166a1d1761ddad4dd924d84a1a91", - "lib/src/migrate/sqlite_checks.dart": "fee6d23a9fae37b2208485fcf2ee24d7e104e65b546162833f12bfebf7457ee3", - "lib/src/migrate/recovery.dart": "df5c9286ca29e3a30ced034e4a2b4c2da7a350d44c6e82cd2d05cdaf7ac87d40", - "lib/src/migrate/catalog.dart": "33071f75cf0965d9074e9d684eecdaa2149182d115cbf8e6bb8d3c8798947265", - "lib/src/migrate/checks.dart": "b523d87cabcfc07eeba2f0295677643752721c37d06824136893bf28164f24c4", - "lib/src/migrate/schema.dart": "d6bdb39a1942dde23e46c24742f8d384d7e3346aa4f60607c326e9ab29721622", - "lib/src/migrate/mysql_catalog.dart": "5b2fb5f741ab5716a421aeadc41fc79539777febd26455345487fe4726c3c749", - "lib/src/migrate/backfill.dart": "a469d869cc8e9eadd17a6341821160d18785bb6400520ac963a60f981726fe49", - "lib/src/migrate/snapshot.dart": "fdf9a58007c77a0dbdcef226077853f8a1327824eff1dd95aaa3fb07b6e94ca4", - "lib/src/migrate/history.dart": "474069de738a00768dc8550ec56534355fb9b4492297bb92677014bef892e9c9", - "lib/src/migrate/mysql_diff.dart": "13b36c45be46d40bf0cc662b436abc8c399564de701b1521ef929a221637ec46", - "lib/src/migrate/computed.dart": "2a1ae36964dee810cee2206f0a57e66090947aaed8ab630947df78886d807443", - "lib/src/migrate/diff.dart": "0c8b6a838b5989de494f09cfb48afc75e8c269b56b3b82fa84daadc1d1032d7a", - "lib/src/migrate/mysql_schema.dart": "e32f9f327bbc63f4b4ceb899c88858ae823428ac53ccace5691b0078c7bd1fa3", - "lib/src/migrate/source.dart": "c084e1b0cd4f3959e34c523db616022ae923a6760ae65be233911a02750ed3c7", - "lib/src/migrate/mysql_recovery.dart": "607695357b1c81f7410a94cd4a52b165474f7348890e88906a612b59a6189ccd", - "lib/src/migrate/step.dart": "8b50c01b26c232e1ca15ad79d4505f74e51693b3ccdccc948bd49bd98276c82a", - "lib/src/query_stream.dart": "d3c33a10cc12c01a95ca313716aefe40334af858944b3bd221be3d672a880bd3", - "lib/src/mysql/values.dart": "b36071d1595441815a59fb2a6b272696013e4cb94c90224872699515fd0328d4", - "lib/src/mysql/connection.dart": "5874e90d91301afe8389f6a6c559dfc056e19570b9fd83b6d7927b64facdf117", - "lib/src/generate/migrations.dart": "0666ae1fc02a5045b42c85e0d2bf37c2e3d02ea9df941f8dc2bb8a3c7ee242cd", - "lib/src/generate/build.dart": "414c9d82e079f84e8429a24d1a112dc6681e4cc01fdd6c7f1dfa3f184ec491d9", - "lib/src/generate/reader.dart": "ab07465c77f3babe1bc43d6dc141a96dcdc56b0d9be755ce5281b1e7663d2bcf", - "lib/src/generate/import.dart": "25a8f9ca4447969daffdef14f5df79c801be34edbf428de2e928a79aa1ab62f3", - "lib/src/generate/types.dart": "ca2c1c2e7810505b18344d140d1b80b550c376afb2b2d7c53d81ac2c6c2dd702", - "lib/src/generate/queries.dart": "7257452e965abdc65602c7d7388a39ed3b151db81a3a174098ec794498c31311", - "lib/src/generate/emitter.dart": "e0015c2dd7f4a7555400f13ef5752afad01576a859379bdcf264cc400d76dfe9", - "lib/src/generate/model.dart": "3145d02a3e6cd0e2f7fd6cdfdb128d1179b9fbe411112da4a2a76027b6d3b6a4", - "lib/src/advanced.dart": "9a0eb4a251864700bb7c860571c06b7bd4e0d85f36ace982f4a5702624dd2684", - "lib/src/named_sql.dart": "334534389cdcd77686362573ae1c286a086bbc493fe727cd040ee51ed70d8c98", - "lib/src/transaction.dart": "f8deb6af0522967accb5a413dfa13879348cb34430b960eab323161ad527bf8e", - "lib/src/relation.dart": "62f348fdc1352fc18996a14817535801b7f8bd619712191bcd860839fdcfe06c", - "lib/src/cursor.dart": "53920951dbb57241d900aca2354daa1b7b87b0b3c84a35e176ddd3b936a5f1d2", - "lib/src/query_reads.dart": "2fe8a1b35f9350c999410eafa870d1c8cbe0268b6d2dd138b2ce5967d8964784", - "lib/src/cte.dart": "cdb8d3086f1f4a5766ce8bd2ac1eaa375fed4283f393bbf94fc33ae29550ff3d", - "lib/src/observation.dart": "8a94800ac1306dd7d8f92f307790c6cb372998649ae59b492488282a20daf292", - "lib/src/plan.dart": "2bdcfa546b8d8633758521ae16dcdcf220932c818370304fa39c700a2101e0b5", - "lib/src/codec.dart": "2efb7aaa13e964a8d17010f40f6f0493e4e77d5840217f7a49f5eb7de4aa723b", - "example/flutter/lib/migrations/migrations.g.dart": "9afb98066f3a62fb2ed3f91e222bd85808f8cd03833b6039b14599bab989806c", - "example/flutter/lib/migrations/m0002_comments.dart": "e95a9e7f16cbb8aaeb53bffafac9bde27d4f05d022b54294caa42283b9468ac0", - "example/flutter/lib/migrations/m0001_initial.dart": "6250ec81d58c25fe6e1b3b99babdcf3b92940c3c60150e46c9d4f8883adebf76", - "example/flutter/lib/schema.orm.dart": "6de2c15529d947aa53fe9a12a930c2aad05391c31e128aeddcf7f6e61cc1f167", - "example/flutter/lib/schema.snapshot.dart": "4ef35433653ac189e16593acc95fa0b09993348824318e8d746609446ca1c913", - "example/flutter/lib/legacy/seed.dart": "5a7956abcaed08b5decac59abc2e15565e6674a1675734a6e261aa0c43d522b9", - "example/flutter/lib/legacy/schema.orm.dart": "36711deab8b639a82c8167046a54b24731710c893ed667a3cc4033d0b2f17471", - "example/flutter/lib/legacy/schema.snapshot.dart": "0dbf13fc969585f5f160fbd77c487e25e8ebf8605865444c9528215ff82cc3f6", - "example/flutter/lib/legacy/schema.dart": "7bdd9b8f8cb97747d632a6da573883cde5fc5cfb60ab1ae2a6bf3da2df36f93a", - "example/flutter/lib/schema.dart": "ee6e7c93f615fcdcd60a020339bbfc0099ab48e0128d12ba20f362685802e0d6", - "example/flutter/lib/main.dart": "2f12f6c0f930a883fd1961da5006ede0674df6e5b6a6f7218a1d4eca5dedf122", - "example/flutter/lib/acceptance.dart": "1a100e938db747835d134fc32cf9893c1e5cd02967b00f8bd15bf1116647536a", - "example/flutter/lib/acceptance_web.dart": "5c749e3b9112ea8b8beb3bc98c8fdc76f92c643c3bd31f51bba2704de10ccd07", - "example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png": "c7c0c0189145e4e32a401c61c9bdc615754b0264e7afae24e834bb81049eaf81", - "example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png": "6a7c8f0d703e3682108f9662f813302236240d3f8f638bb391e32bfb96055fef", - "example/flutter/android/app/src/main/res/drawable/launch_background.xml": "94433756fe10005ddd6fc55cc5c0b1e9f3c10b66a08ceac795ece6713d34e28a", - "example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": "3c34e1f298d0c9ea3455d46db6b7759c8211a49e9ec6e44b635fc5c87dfb4180", - "example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png": "4d470bf22d5c17d84edc5f82516d1ba8a1c09559cd761cefb792f86d9f52b540", - "example/flutter/android/app/src/main/res/values-night/styles.xml": "8c5bad5152f217e837519f9c15211d19f39bd45f5f67faf6dd81cb4b299e518f", - "example/flutter/android/app/src/main/res/values/styles.xml": "937d7c82f539c72eb94fd3a3773f29b3d3fef1997213c92cb632298cedf4bda9", - "example/flutter/android/app/src/main/res/drawable-v21/launch_background.xml": "7ca6d5cec2169c5a98661a119db23a6f3399c8fef2256ff04f3a642831282478", - "example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png": "e14aa40904929bf313fded22cf7e7ffcbf1d1aac4263b5ef1be8bfce650397aa", - "example/flutter/android/app/src/main/AndroidManifest.xml": "6bc850ca03629a837d3dc7b82ba7cbe6c2b969d69ff49a6bea814a651d473c76", - "example/flutter/android/app/src/main/kotlin/dev/orm/orm_flutter/MainActivity.kt": "2c1111a536c7554c4e39eb96dbf1f0a136c51f0dc3a5d9b87c7545f2272a77d8", - "tool/test_flutter.dart": "ce813e2715977ed4202ea7696df7be3789e894dbb290035b9db64052276278f0", - "example/flutter/pubspec.yaml": "af8680b8727885fcd3b8142ca7aea7cde1af6b1c8bee95339c1f09c88ea76632", - "example/flutter/pubspec.lock": "793378bc564741af8e6bc43b774386cafd912ba662201f6e478361f1b09f3840", - "example/flutter/.metadata": "ac4f09e51478c44ffef6b566e3616f16a8b21db8b17eb51969b03cb2e1a2842e", - "example/flutter/android/app/build.gradle.kts": "67cc1825bb2528f74eb6d8577f99f22dd9c23fc8bb61d9be9a80d80e7fa9ecfb", - "example/flutter/android/settings.gradle.kts": "016759283e88ade6eba92910583feeef88b962839b957aa414296afd2952d122", - "example/flutter/android/gradle/wrapper/gradle-wrapper.properties": "b690d26223576fe4e63889fad9a00df81945cb870b9476cc5a563152a1a88a74" - }, - "screenshotSha256": "d12a2ed7e46a352e7eba24cf139b7deb98c887c137a1e89e6bd65d1f239c8b91", - "phases": [ - { - "phase": "legacy", - "schemaVersion": 1, - "release": false, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 5147, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Fresh database", - "Version 1 typed transaction persisted two notes", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "legacyRows": [ - [ - 1, - "Written by version 1", - "2026-09-15 01:02:03.123456+00" - ], - [ - 2, - "Keep this across the upgrade", - "2026-09-15 00:00:00.000000+00" - ] - ], - "history": [ - { - "id": "0001_initial", - "checksum": "eb8478c032ecf4fd28752a34ee1b74b503ae8708c0f6ef851856c63ac02c2252" - } - ], - "status": "passed", - "apkVersionCode": 1 - }, - { - "phase": "upgrade", - "schemaVersion": 2, - "release": true, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 5316, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Existing database retained", - "Known execution phase", - "Expected previous migration history", - "Only version 2 migration applied", - "Live schema matches bundled snapshot", - "Original value and timestamp microseconds preserved", - "New field default / persisted update", - "Initial watch snapshot delivered to Flutter", - "Committed relation write refreshed watch", - "Rollback neither persisted nor notified", - "Typed patch refreshed watch", - "Background SQLite computation result", - "Dart timer and Flutter frames advanced during SQLite work", - "Native SQLite cancellation", - "Worker usable after cancellation", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "unmanaged": [], - "background": { - "elapsedUs": 285090, - "timerTicks": 17, - "flutterFrames": 8 - }, - "watchSnapshots": 3, - "rows": [ - { - "id": 1, - "text": "Written by version 1", - "done": true, - "comments": [] - }, - { - "id": 2, - "text": "Keep this across the upgrade", - "done": false, - "comments": [] - }, - { - "id": 3, - "text": "Written by version 2", - "done": false, - "comments": [ - "Related in one transaction" - ] - } - ], - "history": [ - { - "id": "0001_initial", - "checksum": "eb8478c032ecf4fd28752a34ee1b74b503ae8708c0f6ef851856c63ac02c2252" - }, - { - "id": "0002_comments", - "checksum": "2967a6b4178d2061acad6451e497c7709f82d674a190caa938a765a32accec26" - } - ], - "status": "passed", - "apkVersionCode": 2 - }, - { - "phase": "reopen", - "schemaVersion": 2, - "release": true, - "dart": "3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on \"android_arm64\"", - "os": "AE3A.240806.036", - "androidApi": 35, - "pid": 5390, - "path": "/data/user/0/dev.orm.orm_flutter/files/acceptance.sqlite", - "checks": [ - "Existing database retained", - "Known execution phase", - "Expected previous migration history", - "No migration replay", - "Live schema matches bundled snapshot", - "Original value and timestamp microseconds preserved", - "New field default / persisted update", - "Initial watch snapshot delivered to Flutter", - "Relation data survived process restart", - "Application schema compatibility", - "Closed database reopened read-only" - ], - "sqlite": "3.53.4", - "unmanaged": [], - "watchSnapshots": 1, - "rows": [ - { - "id": 1, - "text": "Written by version 1", - "done": true, - "comments": [] - }, - { - "id": 2, - "text": "Keep this across the upgrade", - "done": false, - "comments": [] - }, - { - "id": 3, - "text": "Written by version 2", - "done": false, - "comments": [ - "Related in one transaction" - ] - } - ], - "history": [ - { - "id": "0001_initial", - "checksum": "eb8478c032ecf4fd28752a34ee1b74b503ae8708c0f6ef851856c63ac02c2252" - }, - { - "id": "0002_comments", - "checksum": "2967a6b4178d2061acad6451e497c7709f82d674a190caa938a765a32accec26" - } - ], - "status": "passed", - "apkVersionCode": 2 - } - ], - "limits": [ - "Android arm64 API 35 emulator; not physical hardware, iOS or macOS Flutter.", - "Legacy APK debug; upgraded/restarted APK release AOT.", - "Frame and timer progress demonstrate worker separation, not an FPS benchmark.", - "Force-stop/relaunch is process persistence evidence, not power-loss testing." - ] -} diff --git a/research/validation/standalone-native.json b/research/validation/standalone-native.json deleted file mode 100644 index 2181374b..00000000 --- a/research/validation/standalone-native.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "capturedAt": "2026-09-19T12:57:23.694966+00:00", - "runtimeSourceCommit": "7a54553933ae1d768576a54a4234b00b6bfcf97e", - "versions": { - "dart": "3.13.3", - "flutter": "3.47.4", - "sqlite": "3.53.4", - "postgres": "18.4", - "mysql": "8.4.11", - "mariadb": "11.8.9", - "chrome": "153" - }, - "native": { - "initialFullSuite": { - "passed": 1119, - "failed": 5, - "duration": "11:50", - "note": "Older CLI JSON expectations and new temporary-table regressions; corrected afterwards." - }, - "finalSessionRegression": { - "passed": 282, - "failed": 0 - }, - "finalCliAndMigrationRegression": { - "passed": 71, - "failed": 0 - }, - "analysis": "No issues found", - "flutterAnalysis": "No issues found" - }, - "localLogDigests": { - "redesign-full.log": { - "sha256": "fa62e954dcd084555aa1d247add8cb4257bdc57f9697d0ea21bd2a85f040ec20" - }, - "redesign-fixes-final.log": { - "sha256": "145c1c2142b550ddba83d1a702dad9e0ea90764016f4069a95420770cb5b658a" - }, - "redesign-session-final.log": { - "sha256": "d8d2eb7b170bbca8ff3059ec5e7c1193aecc9fb09e2af7d25a21fc949533d5ab" - }, - "redesign-analyze-final.log": { - "sha256": "34a0d218671a357b7042bd3be43d0134efd7f8d5b56c5d8c0ab98096c99a331e" - }, - "redesign-flutter-analyze.log": { - "sha256": "40bbb3c2256cfd52d7e7ec89ba44c939374e2f26b6c4aa4324a9146b9ba581a0" - }, - "redesign-postgres-review.log": { - "sha256": "2cdf97fac79070d66670313e16d39b7c333f8326f6060f4114312ba83547a479" - }, - "redesign-version-final.log": { - "sha256": "4eea5b0361952e1c9f644d7ba88b6b288badc637be96d8e70cb42f2affe7c90b" - }, - "redesign-review-analyze.log": { - "sha256": "34a0d218671a357b7042bd3be43d0134efd7f8d5b56c5d8c0ab98096c99a331e" - }, - "linux-sqlite-final.log": { - "sha256": "186f12bb79c650f42e513ed9b73758d8a40b5157cd3cf75f975f6f27e6a07e15" - }, - "sqlite-platform-host.log": { - "sha256": "81cc6734856da124e92eb0a5649a9a53548dcb2e8adb558f5f5131b163b5a8a7" - }, - "redesign-linux-pg-fixtures.log": { - "sha256": "0ba05b46decc78fceb636726374cefb1417101fece8f0bc5311dc6c1a23fe639" - }, - "mysql-statement-timeout.log": { - "sha256": "4d4b114f0189333e7b4adb00ff329f4816c6281cff31b0c1ef44f5fa01ce3860" - }, - "redesign-final-analyze.log": { - "sha256": "34a0d218671a357b7042bd3be43d0134efd7f8d5b56c5d8c0ab98096c99a331e" - }, - "linux-sqlite-capabilities-final.log": { - "sha256": "13a02dd8b95df6ffa238a966089c2b16622d81482e0643e8d1bcf3610ea1eb6d" - }, - "sqlite-capabilities-host-final.log": { - "sha256": "e9d51ff36d51b734d67555b280a29acb8fb029f419af92c1660f3a8ed05e50f3" - }, - "ci-35446420359.log": { - "sha256": "5a40050575da4445c076f65a1ab7f025e2614569efa8e442ad496f7d31f58054" - }, - "ci-35448372437.log": { - "sha256": "f28d359e9d376ff16bd55247014df42c79235613d65b261e10466182ca0217f0" - } - }, - "limits": [ - "The final focused invocations are not described as a second complete suite; GitHub CI runs the whole suite as a merge gate.", - "Fresh platform records independently pin the runtime source and build inputs." - ], - "reviewRegression": { - "capturedAt": "2026-09-19T13:23:27.946868+00:00", - "runtimeSourceCommit": "46265f836e49a248c7e6a64dbc646fada929e79f", - "postgres": { - "passed": 176, - "failed": 0, - "newProxyRegressions": 4 - }, - "mysqlDriverAndVersionGate": { - "passed": 40, - "failed": 0 - }, - "analysis": "No issues found" - }, - "portabilityRegression": { - "capturedAt": "2026-09-19T13:33:22.424576+00:00", - "runtimeSourceCommit": "46265f836e49a248c7e6a64dbc646fada929e79f", - "linux": { - "platform": "linux_arm64", - "dart": "3.13.3", - "passed": 21, - "skipped": 23, - "skipReason": "8 statement-interruption scenarios and 15 bounded retries require sqlite3_interrupt, absent from the default Linux asset. Explicit rejection is verified.", - "aotBundles": [ - "native_execution", - "native_retry" - ] - }, - "macosAndPostgres": { - "passed": 84, - "skipped": 4, - "skipReason": "Inverse unsupported-capability checks on drivers that support interruption." - }, - "postgresFixtures": { - "passed": 48, - "failed": 0 - } - }, - "statementTimeoutRegression": { - "capturedAt": "2026-09-19T13:38:04.781896+00:00", - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "mysqlAndMariadb": { - "passed": 140, - "failed": 0 - }, - "analysis": "No issues found", - "independentCodeReview": "No additional concrete defects found" - }, - "linuxCompleteAttempt": { - "headCommit": "326f41e842acc840e15b4f8cd28bf159df803995", - "runUrl": "https://github.com/medz/dart-orm/actions/runs/35446420359", - "passed": 1129, - "failed": 17, - "skipped": 25, - "note": "All 17 failures were remaining Linux SQLite interruption assumptions in transaction/acquisition/watch tests. Runtime source was unchanged by their correction. The final PR CI run is the complete merge gate." - }, - "finalCapabilityRegression": { - "capturedAt": "2026-09-19T14:19:05.153644+00:00", - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "linux": { - "passed": 41, - "skipped": 14, - "skipReason": "13 transaction interruption and 1 active watched-query interruption scenarios." - }, - "macosAndPostgres": { - "passed": 108, - "skipped": 4, - "skipReason": "Inverse unsupported-capability tests." - } - }, - "linuxNativeAcceptance": { - "headCommit": "e933576a790f579477e9c8a1980eb74febe8742a", - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "runUrl": "https://github.com/medz/dart-orm/actions/runs/35448372437", - "passed": 1136, - "failed": 0, - "skipped": 41, - "skipReason": "Native interruption-dependent scenarios and inverse unsupported-capability checks.", - "browserJs": { - "scenariosPassed": 21, - "runnerSucceeded": false, - "reason": "Chrome profile deletion raced with exiting child processes (ENOTEMPTY)." - }, - "note": "Complete native stage succeeded. The overall workflow failed during browser runner cleanup, so the final corrected workflow remains the merge gate." - }, - "browserCleanupRegression": { - "runtimeSourceCommit": "3aef4e7905f6997ff2fa9c181709bdddbdcc64ad", - "helperTestsPassed": 5, - "chromeJs": { - "passed": 21, - "runnerExitCode": 0 - }, - "chromeWasm": { - "passed": 21, - "runnerExitCode": 0 - }, - "flutterWeb": { - "js": 21, - "wasmNonIsolated": 21, - "wasmIsolated": 21, - "runnerExitCode": 0 - }, - "logs": { - "browser-profile-tests.log": { - "sha256": "7bbac204502db57154e0cefbd95a2dfd56a6bb8590fa0c0b1d37287d63ac5876" - }, - "browser-cleanup-js.log": { - "sha256": "dee57767d28ddaf07db9be34aba8104c62f533183d36871461da723ab74ff6ab" - }, - "browser-cleanup-wasm.log": { - "sha256": "3f650ea848b0964e753e496833574ae59e1d73c39254767645878140471c1235" - }, - "flutter-web-cleanup.log": { - "sha256": "cd6260d768cddd523c3eb80d6eace8f7399287f9d53986d75e3d893a2f410a44" - } - }, - "note": "Five real Chrome runs completed profile cleanup on macOS. Independent Linux browser CI remains required for the final head." - } -} diff --git a/research/validation/standalone-redesign.md b/research/validation/standalone-redesign.md deleted file mode 100644 index df68e2f5..00000000 --- a/research/validation/standalone-redesign.md +++ /dev/null @@ -1,123 +0,0 @@ -# Standalone ORM acceptance - -Captured on 2026-09-19. Initial runtime source: `7a54553933ae1d768576a54a4234b00b6bfcf97e`. -Final runtime and refreshed Web assets: `3aef4e7905f6997ff2fa9c181709bdddbdcc64ad`. -The refreshed browser reports pin the latter revision. Android reports retain the -initial revision. Later changes affect server drivers, operation capability -validation, test portability and Web asset fingerprints; Android was not rerun. -Validation commits add reports without changing runtime. - -## Actual environments - -Dart 3.13.3 and Flutter 3.47.4 stable on macOS arm64; SQLite 3.53.4; -PostgreSQL 18.4; MySQL 8.4.11 and MariaDB 11.8.9 in separate disposable containers. -MySQL/MariaDB tests require TLS with their fixture certificates. Flutter and -plain Dart browser tests use actual Chrome 153, not simulated DOM APIs. - -## Results and scope - -| Validation | Evidence | -| --- | --- | -| Native full regression, first invocation | 1119 passed, 5 failed in 11:50. Failures were two older CLI JSON expectations and three new temporary-metadata regressions. [Structured record](standalone-native.json). | -| Final session/transaction regression | 282 passed. Real SQLite/PostgreSQL session ownership, escaped connections, swallowed SQL failures, pending work, cursor cleanup, transaction/retry/acquisition/watch paths; MySQL/MariaDB transaction boundaries included. | -| Final CLI/import/migration/SQL regression | 71 passed. All four engines initialize and create Dart history; SQLite lifecycle and SQLite/PostgreSQL adoption use real consumers; MySQL/MariaDB test actual DDL/backfill recovery and metadata shadowing. | -| PostgreSQL review regression | 176 passed, including four new real TCP-proxy regressions for explicit/default timeout, cancellation before PID discovery and a shared PID/application deadline. All four new cases failed before the fix. | -| MySQL version boundary | 40 driver/migration-target checks passed. MySQL driver and migration support now both require 8.4+; older migration targets fail before locks, journals or DDL. | -| Static checks | Root Dart analysis and Flutter example analysis: no issues. Formatting and diff whitespace checks pass. | -| Plain Dart Web | JS and WASM each pass 21 scenarios. [Reports](standalone-browser.json). | -| Flutter Web release | JS, WASM without isolation and WASM with isolation each pass 21 scenarios. Assets are bundled automatically; nested routes, reload recovery, migrations and animation progress during SQL are checked. [Reports](standalone-flutter-web.json). | -| Flutter Android arm64 API 35 | Legacy debug installation: 4 checks; release AOT APK upgrade: 17; new-process reopen: 11. Host checks distinct process IDs, retained database path/data/history and APK version codes. [Report](standalone-flutter.json), [screenshot](standalone-flutter-android.png). | -| Native packaging | Both Android APKs contain arm64 `libsqlite3.so` and zero SQLite Web worker/WASM assets. | - -The initial full run and subsequent targeted runs are separate invocations. They -are not combined into a claimed all-green full run. The PR's GitHub CI check runs -formatting, analysis, resource fingerprints, the complete native suite with all -four engines enabled, and Chrome JS/WASM. Passing that check and Codex review of -the final head are required before merge; the PR records their results. - -## Correctness findings resolved before platform capture - -Independent review corrected MySQL/MariaDB registry parsing and non-atomic plan -reporting, temporary migration metadata shadowing, and borrowed-session ownership -and error tracking. A caught raw PostgreSQL error cannot make an aborted -transaction report success. Borrowed connections and cursors cannot escape their -session/savepoint; unawaited accepted work drains before the boundary completes. -MySQL/MariaDB migration failures and uncertain commits use durable checkpoints and -catalog verification rather than assuming that DDL rolls back. - -## Codex review correction - -The first external Codex review found an unbounded first PostgreSQL backend PID -lookup. The timer and cancellation listener now start before that lookup; an -interruption without a PID discards the connection before sending application SQL. -The cached PID path retains cancellation and reuse, and uncertain errors after a -statement starts are not reclassified as confirmed interruption. A real PostgreSQL -TCP proxy reproduces the original hang and verifies the correction. - -The second Codex review found that MySQL/MariaDB statement deadlines were -implemented by the driver but rejected by the runtime cancellation gate. -`Capabilities.statementTimeout` now distinguishes connection-discard deadlines -from token cancellation. The raw runtime and ORM accept per-operation timeouts; -transaction deadlines and retries still require actual interruption. All 140 -driver, typed database and transaction-boundary regressions pass against real -MySQL/MariaDB, including unknown-outcome messaging and caught-timeout failure. - -## Linux CI portability corrections - -The first Linux CI run revealed test assumptions hidden by macOS: borrowed -PostgreSQL fixtures omitted URL passwords, an import fixture used `compile exe` -without bundling native assets, and interruption tests assumed every SQLite build -exports `sqlite3_interrupt`. These are corrected without substituting another -SQLite library or changing the runtime capability contract. The PostgreSQL temporal -and acquisition regression run passes 48 checks after the fixture correction. -Linux arm64 passes 21 focused checks and two real AOT bundles, with 23 explicit -capability skips (8 interruption scenarios and 15 bounded retries). The same -focused suites on macOS/PostgreSQL pass 84 checks, with four inverse unsupported- -capability checks skipped. These platform runs use the unchanged `46265f83` runtime -plus the portability test fixtures. [Structured record](standalone-native.json). - -CLI workflows kept progressing in the original run; repeated independent Dart -starts made the full run longer than local acceptance. CI now permits 45 minutes -without removing checks. Canceled earlier runs are not passing acceptance evidence. - -The complete Linux run on `326f41e8` then reported 1129 passed, 17 failed and -25 skipped. All remaining failures were interruption assumptions in transaction, -acquisition and watch tests. Their correction preserves ordinary COMMIT failure, -unknown acknowledgement, acquisition and subscription checks; it only skips actual -interruption scenarios and explicitly verifies unsupported controls before SQL. -The corrected three suites pass 41 tests with 14 capability skips on Linux, and -108 tests with four inverse-capability skips on macOS/PostgreSQL. -Runtime and Web assets remain identical to `3aef4e79`. The final PR run, not this -failed attempt, is the complete merge gate. - -The subsequent complete native Linux run on `e933576a` succeeds: **1136 passed, -41 capability skips, zero failures**. Its Chrome JS report also passes all 21 -scenarios, but the runner exits unsuccessfully when profile deletion races with -exiting Chrome child processes. Runner cleanup is corrected with bounded retries, -and browser checks run as a separate CI job so their outcome is visible early. -The cleanup correction passes five deterministic regressions and five real Chrome -runs on macOS: Dart JS/WASM and Flutter Web JS/WASM with and without isolation -each pass 21 scenarios, including successful runner exit and profile deletion. -The final workflow must pass both jobs before merge. - -## Explicit boundaries - -MySQL/MariaDB provide exact values, storage, comparison and MIN/MAX within their -physical limits. Typed arithmetic, SUM, decimal set operations and explicit -precision narrowing that can silently lose digits are rejected; see the -[reproductions](../mysql-precision-boundaries.md) and [driver contract](../../doc/mysql.md). -Their initial adapters own a single queued connection and do not support streaming -or cancellation. Runtime leases add a cleanup ROLLBACK round trip. DDL remains -non-atomic and uses the [engine-specific recovery workflow](../../doc/mysql-migrations.md). - -The default Linux native asset in `sqlite3` 3.6.0 hides `sqlite3_interrupt`. -Statement cancellation, execution deadlines and bounded retries are rejected -before SQL; ordinary database operations and streaming remain supported. Tests -check that refusal explicitly and skip only scenarios requiring interruption. -Native executables must use `dart build cli` and ship the whole bundle. - -Chrome and an Android emulator do not certify Safari, Firefox, physical devices, -iOS or macOS Flutter. Timer/animation progress establishes worker separation, -not a frame-rate or throughput improvement. Historical benchmarks and validation -files retain their original source revisions; this change makes no new performance -claim. No package was published. diff --git a/research/validation/usage.json b/research/validation/usage.json deleted file mode 100644 index e2f68eac..00000000 --- a/research/validation/usage.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "capturedAt": "2026-09-15T10:03:19.386980+00:00", - "sourceBase": "20ffee7", - "commands": [ - { - "name": "dependencies", - "command": [ - "dart", - "pub", - "get", - "--offline" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 0.379, - "stdout": "Resolving dependencies...\nDownloading packages...\nGot dependencies!\n", - "stderr": "" - }, - { - "name": "generate", - "command": [ - "dart", - "run", - "orm", - "generate", - "example/schema.dart" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 3.588, - "stdout": "Generated example/schema.orm.dart\n", - "stderr": "Running build hooks...Running build hooks..." - }, - { - "name": "quickstart", - "command": [ - "dart", - "run", - "example/main.dart" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 1.503, - "stdout": "[(email: seven@example.com, titles: [Hello Dart])]\n", - "stderr": "Running build hooks...Running build hooks..." - }, - { - "name": "many_to_many", - "command": [ - "dart", - "run", - "example/teams/main.dart" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 1.139, - "stdout": "2 SQL templates; 32764 parent keys per batch\n[(name: Ada, teams: [(role: MembershipRole.member, team: Docs), (role: MembershipRole.owner, team: Core)])]\n2 SQL statements; [1, 2] rows\n1 acquisition; 2 decode batches\n", - "stderr": "Running build hooks...Running build hooks..." - }, - { - "name": "sqlite_queries", - "command": [ - "dart", - "run", - "example/queries.dart" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 1.442, - "stdout": "{\"backend\":\"sqlite\",\"checks\":[\"Dynamic typed filter and scalar selection\",\"Joined field orders root rows\",\"Correlated relation count orders root rows\",\"Grouping, HAVING and CTE exported columns\",\"Window ordering is independent of result ordering\",\"Typed IN subquery\",\"Correlated scalar aggregate\",\"Versioned cursor preserves NULL order and unique tie breaker\",\"Generated schema matches the live database\"],\"status\":\"passed\"}\n", - "stderr": "Running build hooks...Running build hooks...", - "report": { - "backend": "sqlite", - "checks": [ - "Dynamic typed filter and scalar selection", - "Joined field orders root rows", - "Correlated relation count orders root rows", - "Grouping, HAVING and CTE exported columns", - "Window ordering is independent of result ordering", - "Typed IN subquery", - "Correlated scalar aggregate", - "Versioned cursor preserves NULL order and unique tie breaker", - "Generated schema matches the live database" - ], - "status": "passed" - } - }, - { - "name": "postgres_queries", - "command": [ - "dart", - "run", - "example/queries.dart" - ], - "environmentKeys": [ - "ORM_EXAMPLE_POSTGRES" - ], - "exitCode": 0, - "elapsedSeconds": 1.26, - "stdout": "{\"backend\":\"postgres\",\"checks\":[\"Dynamic typed filter and scalar selection\",\"Joined field orders root rows\",\"Correlated relation count orders root rows\",\"Grouping, HAVING and CTE exported columns\",\"Window ordering is independent of result ordering\",\"Typed IN subquery\",\"Correlated scalar aggregate\",\"Versioned cursor preserves NULL order and unique tie breaker\",\"Generated schema matches the live database\"],\"status\":\"passed\"}\n", - "stderr": "Running build hooks...Running build hooks...", - "report": { - "backend": "postgres", - "checks": [ - "Dynamic typed filter and scalar selection", - "Joined field orders root rows", - "Correlated relation count orders root rows", - "Grouping, HAVING and CTE exported columns", - "Window ordering is independent of result ordering", - "Typed IN subquery", - "Correlated scalar aggregate", - "Versioned cursor preserves NULL order and unique tie breaker", - "Generated schema matches the live database" - ], - "status": "passed" - } - }, - { - "name": "migration_chain", - "command": [ - "dart", - "run", - "orm", - "migration", - "check", - "--dir", - "example/flutter/assets/migrations" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 0.476, - "stdout": "{\n \"valid\": true,\n \"dialect\": \"sqlite\",\n \"migrations\": [\n \"0001_initial\",\n \"0002_comments\"\n ]\n}\n", - "stderr": "Running build hooks...Running build hooks..." - }, - { - "name": "analysis", - "command": [ - "dart", - "analyze" - ], - "environmentKeys": [], - "exitCode": 0, - "elapsedSeconds": 1.341, - "stdout": "Analyzing dart-orm...\nNo issues found!\n", - "stderr": "" - } - ], - "sources": { - "example/main.dart": "82eb04e907bfcb02deabaead133cc4ddcf149d2fb5eba84ccbd91694a82ec83a", - "example/teams/main.dart": "655d58b82d10fdceaed987beb2dd877dc61afd951d9889c62880b352982a3d14", - "example/queries.dart": "c9395fdf6f9be194c8566fea8476e5fba724bba5906a42faa8b9d6eb653e56f1", - "example/schema.dart": "d164d87b21a62cd5d97f074dc1029f36725a622aa0ab88643215a9496bbb279e", - "example/schema.orm.dart": "ac37c40746b9c419378c71a6a25488fc658a2e6cc9e82ccd53b262ffe508b627", - "example/schema.orm.json": "97fd6dbd1c731b86e10dfe58b75dbecafe27c753f9795a396c8b419137f23683", - "lib/src/migrate/catalog.dart": "80c1a396727d57e9c49e37914c076e6fab64a3eb43437b522c82c749cf280359" - } -} diff --git a/test/cardinality_test.dart b/test/cardinality_test.dart new file mode 100644 index 00000000..71956c27 --- /dev/null +++ b/test/cardinality_test.dart @@ -0,0 +1,217 @@ +import 'package:orm/sqlite.dart'; +import 'package:test/test.dart'; + +enum _Read { first, firstOrNull, single, singleOrNull } + +void main() { + late Database db; + + setUp(() async { + db = Database(await SqliteDriver.open(const SqliteOptions.memory())); + await db.execute( + SqlCommand( + 'CREATE TABLE cardinality (id INTEGER PRIMARY KEY, label TEXT, hits INTEGER NOT NULL)', + ), + ); + }); + tearDown(() => db.close()); + + Future seed(int count) async { + for (var id = 1; id <= count; id++) { + await db.execute( + SqlCommand( + 'INSERT INTO cardinality (id, label, hits) VALUES (?, NULL, 0)', + [id], + ), + ); + } + } + + for (final count in [0, 1, 2]) { + for (final read in _Read.values) { + test('Query ${read.name} distinguishes $count rows', () async { + await seed(count); + final query = db + .table(_table) + .orderBy((row) => [row.id.asc()]) + .select((row) => row.id); + final result = _readQuery(query, read); + if (_rejects(read, count)) { + await expectLater(result, throwsA(_cardinality)); + } else { + expect(await result, count == 0 ? null : 1); + } + }); + + test( + 'Returning ${read.name} checks $count rows after the entire write', + () async { + await seed(count); + final returning = db + .table(_table) + .update((row) => [row.hits.increment(1)]) + .returning((row) => row.id); + final result = _readReturning(returning, read); + if (_rejects(read, count)) { + await expectLater(result, throwsA(_cardinality)); + } else if (count == 0) { + expect(await result, isNull); + } else { + expect(await result, isIn(List.generate(count, (i) => i + 1))); + } + // RETURNING cardinality is a Dart result check, not a SQL row limit. + expect( + await db.table(_table).select((row) => row.hits).get(), + List.filled(count, 1), + ); + }, + ); + } + } + + for (final read in _Read.values) { + test('Query ${read.name} counts SQL NULL as a row', () async { + await seed(1); + expect( + await _readQuery(db.table(_table).select((row) => row.label), read), + isNull, + ); + if (read == _Read.single || read == _Read.singleOrNull) { + await db.execute( + SqlCommand('INSERT INTO cardinality VALUES (2, NULL, 0)'), + ); + await expectLater( + _readQuery(db.table(_table).select((row) => row.label), read), + throwsA(_cardinality), + ); + } + }); + + test('Returning ${read.name} counts SQL NULL as a row', () async { + await seed(1); + final returning = db + .table(_table) + .update((row) => [row.hits.increment(1)]) + .returning((row) => row.label); + expect(await _readReturning(returning, read), isNull); + if (read == _Read.single || read == _Read.singleOrNull) { + await db.execute( + SqlCommand('INSERT INTO cardinality VALUES (2, NULL, 0)'), + ); + await expectLater( + _readReturning(returning, read), + throwsA(_cardinality), + ); + expect( + await db + .table(_table) + .orderBy((row) => [row.id.asc()]) + .select((row) => row.hits) + .get(), + [2, 1], + ); + } + }); + } + + test('read cardinality respects an explicit limit and offset', () async { + await seed(2); + final query = db + .table(_table) + .orderBy((row) => [row.id.asc()]) + .select((row) => row.id); + expect(await query.take(1).single(), 1); + expect(await query.take(1).singleOrNull(), 1); + expect(await query.skip(1).single(), 2); + expect(await query.skip(2).singleOrNull(), isNull); + expect(await query.take(0).firstOrNull(), isNull); + expect(await query.take(0).singleOrNull(), isNull); + await expectLater(query.take(0).first(), throwsA(_cardinality)); + await expectLater(query.take(0).single(), throwsA(_cardinality)); + }); + + for (final read in [_Read.single, _Read.singleOrNull]) { + test( + 'Returning ${read.name} failure rolls back an enclosing transaction', + () async { + await seed(2); + await expectLater( + db.transaction( + (tx) => _readReturning( + tx + .table(_table) + .update((row) => [row.hits.increment(1)]) + .returning((row) => row.id), + read, + ), + ), + throwsA(_cardinality), + ); + expect(await db.table(_table).select((row) => row.hits).get(), [0, 0]); + }, + ); + } + + test('a strict empty read rolls back preceding transaction writes', () async { + await seed(1); + await expectLater( + db.transaction((tx) async { + await tx + .table(_table) + .update((row) => [row.hits.increment(1)]) + .execute(); + return tx.table(_table).where((row) => row.id.eq(999)).first(); + }), + throwsA(_cardinality), + ); + expect(await db.table(_table).select((row) => row.hits).single(), 0); + }); +} + +bool _rejects(_Read read, int count) => switch (read) { + _Read.first => count == 0, + _Read.firstOrNull => false, + _Read.single => count != 1, + _Read.singleOrNull => count > 1, +}; + +Future _readQuery(Query query, _Read read) => switch (read) { + _Read.first => query.first(), + _Read.firstOrNull => query.firstOrNull(), + _Read.single => query.single(), + _Read.singleOrNull => query.singleOrNull(), +}; + +Future _readReturning(Returning returning, _Read read) => + switch (read) { + _Read.first => returning.first(), + _Read.firstOrNull => returning.firstOrNull(), + _Read.single => returning.single(), + _Read.singleOrNull => returning.singleOrNull(), + }; + +final _cardinality = isA().having( + (error) => error.code, + 'code', + 'QUERY.CARDINALITY', +); +final _id = Column('id', Codecs.integer); +final _label = Column('label', Codecs.text.nullable(), nullable: true); +final _hits = Column('hits', Codecs.integer); +final _schema = TableSchema( + 'cardinality', + columns: [_id, _label, _hits], + primaryKey: ['id'], +); +final _table = Table( + _schema, + _Fields.new, + (row) => (row.id, row.label, row.hits).row, +); + +final class _Fields extends Fields { + _Fields(super.table); + late final id = column(_id); + late final label = column(_label); + late final hits = column(_hits); +} diff --git a/test/database_test.dart b/test/database_test.dart index fa6efab8..79c1a9f5 100644 --- a/test/database_test.dart +++ b/test/database_test.dart @@ -93,7 +93,7 @@ void runDatabaseTests(String name, Future> Function() open) { expect(await base.skip(2).select((u) => u.id).get(), [3, 2, 1]); expect(await base.where((u) => u.id.isIn([])).exists(), false); expect(await base.where((u) => u.nickname.isNull()).count(), 5); - expect(await base.take(0).first(), null); + expect(await base.take(0).firstOrNull(), null); expect(base.single(), throwsA(isA())); }); @@ -244,7 +244,7 @@ void runDatabaseTests(String name, Future> Function() open) { .insert((u) => [u.email.set('same')]) .onConflictDoNothing(target: (u) => [u.email]) .returning((u) => u.id) - .first(); + .firstOrNull(); expect(skipped, null); expect(await db.table(users).count(), 1); expect( diff --git a/test/layer_boundaries_test.dart b/test/layer_boundaries_test.dart index 75d73488..b3e537ac 100644 --- a/test/layer_boundaries_test.dart +++ b/test/layer_boundaries_test.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:orm/driver.dart' as raw; import 'package:orm/migrate.dart' as migrations; @@ -11,6 +13,34 @@ import 'package:orm/values.dart' as values; import 'package:test/test.dart'; void main() { + test( + 'libraries, executables, and generated fixtures are standalone Dart', + () { + final files = [ + for (final root in ['lib', 'bin', 'tool', 'example', 'test']) + ..._dartFiles(Directory(root)), + ]; + expect(files, isNotEmpty); + expect(files.any((file) => file.path.endsWith('.orm.dart')), isTrue); + expect(files.any((file) => file.path.endsWith('.snapshot.dart')), isTrue); + expect(files.any((file) => file.path.endsWith('.queries.dart')), isTrue); + for (final file in files) { + final unit = parseString( + content: file.readAsStringSync(), + path: file.path, + ).unit; + expect( + unit.directives.where( + (directive) => + directive is PartDirective || directive is PartOfDirective, + ), + isEmpty, + reason: '${file.path} must import and export standalone libraries', + ); + } + }, + ); + test('values and metadata can be authored without a query or session', () { final decimal = values.Decimal.parse('9007199254740993.01'); expect( @@ -89,6 +119,29 @@ void main() { return null; } + // Internal imports must obey the same boundaries as public facades. + // Otherwise importing src/query directly could evade the SQL-layer ban. + String layer(String name) { + if (name == 'src/schema/model.dart') return 'schema_model.dart'; + if (name == 'src/schema/declaration.dart') return 'schema.dart'; + for (final (prefix, entry) in [ + ('src/values/', 'values.dart'), + ('src/driver/', 'driver.dart'), + ('src/query/', 'sql.dart'), + ('src/runtime/', 'runtime.dart'), + ('src/orm/', 'orm.dart'), + ('src/migrate/', 'migrate.dart'), + ('src/generate/', 'generate.dart'), + ('src/cli/', 'cli.dart'), + ('src/sqlite/', 'drivers/sqlite.dart'), + ('src/postgres/', 'drivers/postgres.dart'), + ('src/mysql/', 'drivers/mysql.dart'), + ]) { + if (name.startsWith(prefix)) return entry; + } + return name; + } + Set dependencies(LibraryElement entry) { final visited = {}; void visit(LibraryElement library) { @@ -202,7 +255,11 @@ void main() { }; for (final entry in forbidden.entries) { final closure = dependencies(await resolve(entry.key)); - final local = closure.map(localName).whereType().toSet(); + final local = closure + .map(localName) + .whereType() + .map(layer) + .toSet(); expect( local.intersection(entry.value), isEmpty, @@ -235,6 +292,19 @@ void main() { ); } +Iterable _dartFiles(Directory directory) sync* { + for (final entry in directory.listSync(followLinks: false)) { + if (entry is File && entry.path.endsWith('.dart')) { + yield entry; + } else if (entry is Directory) { + final name = entry.path.split(Platform.pathSeparator).last; + if (name != 'build' && !name.startsWith('.')) { + yield* _dartFiles(entry); + } + } + } +} + final _id = schema.Column('id', values.Codecs.integer); final _schema = schema.TableSchema( 'independent', diff --git a/test/mysql_import_test.dart b/test/mysql_import_test.dart index cf650c87..1a3fc8b3 100644 --- a/test/mysql_import_test.dart +++ b/test/mysql_import_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:orm/drivers/mysql.dart'; +import 'package:orm/drivers/mariadb.dart'; import 'package:orm/generate.dart'; import 'package:orm/migrate.dart'; import 'package:orm/runtime.dart'; diff --git a/test/mysql_migration_test.dart b/test/mysql_migration_test.dart index ab89f338..8c7be3a3 100644 --- a/test/mysql_migration_test.dart +++ b/test/mysql_migration_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:orm/drivers/mysql.dart'; +import 'package:orm/drivers/mariadb.dart'; import 'package:orm/migrate.dart'; import 'package:orm/runtime.dart'; import 'package:test/test.dart'; diff --git a/test/mysql_transaction_boundary_test.dart b/test/mysql_transaction_boundary_test.dart index 7d73c547..faa00e68 100644 --- a/test/mysql_transaction_boundary_test.dart +++ b/test/mysql_transaction_boundary_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:orm/drivers/mysql.dart'; +import 'package:orm/drivers/mariadb.dart'; import 'package:orm/runtime.dart'; import 'package:test/test.dart'; diff --git a/test/public_api_test.dart b/test/public_api_test.dart index 3711e897..609359b1 100644 --- a/test/public_api_test.dart +++ b/test/public_api_test.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:test/test.dart'; @@ -57,8 +59,35 @@ void main() { ), ), ); - expect(exports(schema), isNot(contains('Database'))); + expect( + exports(schema).intersection({ + 'Database', + 'TableSchema', + 'Column', + 'ForeignKey', + 'IndexSchema', + 'CheckSchema', + }), + isEmpty, + reason: 'Declarations do not expose physical schema construction.', + ); + expect( + exports(schema), + containsAll(['entity', 'Id', 'Codec', 'Decimal', 'ComputedStorage']), + ); expect(exports(runtime), isNot(contains('Migration'))); + for (final path in ['lib/mysql.dart', 'lib/drivers/mysql.dart']) { + final names = exports(await library(path)); + expect(names, containsAll(['MysqlDriver', 'MysqlOptions'])); + expect(names, isNot(contains('MariadbDriver'))); + expect(names, isNot(contains('MariadbOptions'))); + } + for (final path in ['lib/mariadb.dart', 'lib/drivers/mariadb.dart']) { + final names = exports(await library(path)); + expect(names, containsAll(['MariadbDriver', 'MariadbOptions'])); + expect(names, isNot(contains('MysqlDriver'))); + expect(names, isNot(contains('MysqlOptions'))); + } expect( exports(migrate), containsAll(['Migration', 'SqlDialect', 'TableSchema', 'Codecs']), @@ -66,6 +95,72 @@ void main() { expect(exports(migrate), isNot(contains('generateSchema'))); expect(exports(generate), isNot(contains('ormBuilder'))); expect(exports(builder), {'ormBuilder', 'ormQueryBuilder'}); + + // Resolve every public library, including newly added entrypoints. + // A show list is required whenever a facade reaches into src directly. + final entrypoints = [ + ...Directory('lib').listSync().whereType(), + ...Directory('lib/drivers').listSync().whereType(), + ].where((file) => file.path.endsWith('.dart')); + const implementationNames = { + 'SqlNode', + 'SqlWriter', + 'ColumnNode', + 'ParameterNode', + 'QueryState', + 'SelectionPlan', + 'RowDecoder', + 'Join', + 'CteDefinition', + 'UnionSource', + 'ReadTables', + 'MutationKind', + 'RelationBinding', + 'TypedRelationBinding', + 'ConnectionWait', + 'TransactionControl', + 'BackfillBudget', + 'BackfillPaused', + 'quoteIdentifier', + 'migrationHash', + 'readBackfillProgress', + 'diffSchema', + 'SqliteExecutor', + 'OpenedSqlite', + 'sqliteWorkerBuild', + 'databaseMembers', + 'isMysqlDialect', + }; + for (final file in entrypoints) { + final entry = await library(file.path); + final namespace = entry.exportNamespace.definedNames2; + expect( + namespace.keys.toSet().intersection(implementationNames), + isEmpty, + reason: '${file.path} exports an implementation detail', + ); + expect( + namespace.entries + .where((entry) => entry.value.metadata.hasInternal) + .map((entry) => entry.key), + isEmpty, + reason: '${file.path} exports an @internal declaration', + ); + final unit = parseString( + content: file.readAsStringSync(), + path: file.path, + ).unit; + for (final directive + in unit.directives.whereType()) { + if (directive.uri.stringValue!.split('/').contains('src')) { + expect( + directive.combinators.whereType(), + isNotEmpty, + reason: '${file.path} broadly exports ${directive.uri}', + ); + } + } + } final client = exports(await library('example/schema.orm.dart')); expect( client, diff --git a/tool/benchmark_editor.dart b/tool/benchmark_editor.dart index 0a520ba1..7ec1b206 100644 --- a/tool/benchmark_editor.dart +++ b/tool/benchmark_editor.dart @@ -373,8 +373,8 @@ Future main(List args) async { }; final output = File( smoke - ? '.dart_tool/editor${sameSession ? '-recovery' : ''}-smoke.json' - : 'research/benchmarks/editor${sameSession ? '-recovery' : ''}.json', + ? '.dart_tool/benchmarks/editor${sameSession ? '-recovery' : ''}-smoke.json' + : '.dart_tool/benchmarks/editor${sameSession ? '-recovery' : ''}.json', ); await output.parent.create(recursive: true); await output.writeAsString( diff --git a/tool/benchmark_generation.dart b/tool/benchmark_generation.dart index 0354a1ee..5812237d 100644 --- a/tool/benchmark_generation.dart +++ b/tool/benchmark_generation.dart @@ -5,7 +5,7 @@ import 'src/build_fixture.dart'; Future main(List args) async { final output = args.isEmpty - ? 'research/benchmarks/generation.json' + ? '.dart_tool/benchmarks/generation.json' : args.single; final versions = await packageVersions(); final results = >[]; diff --git a/tool/benchmark_runtime.dart b/tool/benchmark_runtime.dart index ae9cceef..2c131190 100644 --- a/tool/benchmark_runtime.dart +++ b/tool/benchmark_runtime.dart @@ -237,8 +237,8 @@ Future main(List args) async { final output = File( outputPath ?? (smoke - ? '.dart_tool/runtime-smoke.json' - : 'research/benchmarks/runtime.json'), + ? '.dart_tool/benchmarks/runtime-smoke.json' + : '.dart_tool/benchmarks/runtime.json'), ); await output.parent.create(recursive: true); await output.writeAsString( diff --git a/tool/compare_authoring.dart b/tool/compare_authoring.dart index 7378f45d..4b80dc23 100644 --- a/tool/compare_authoring.dart +++ b/tool/compare_authoring.dart @@ -304,10 +304,13 @@ Future main(List args) async { 'results': results, }; final path = smoke - ? '.dart_tool/authoring-smoke.json' - : 'research/benchmarks/authoring.json'; - await File(path) - .writeAsString('${const JsonEncoder.withIndent(' ').convert(report)}\n'); + ? '.dart_tool/benchmarks/authoring-smoke.json' + : '.dart_tool/benchmarks/authoring.json'; + final output = File(path); + await output.parent.create(recursive: true); + await output.writeAsString( + '${const JsonEncoder.withIndent(' ').convert(report)}\n', + ); stdout.writeln('Saved $path'); } diff --git a/tool/src/runtime_vm.dart b/tool/src/runtime_vm.dart index c7552131..d81ed1d0 100644 --- a/tool/src/runtime_vm.dart +++ b/tool/src/runtime_vm.dart @@ -61,7 +61,7 @@ final class RuntimeVm { '_List', '_GrowableList', '_Map', - '_SelectionPlan', + 'SelectionPlan', }.contains((row['class'] as Map)['name'])) { 'name': (row['class'] as Map)['name'], @@ -82,7 +82,7 @@ final class RuntimeVm { '_List', '_GrowableList', '_Map', - '_SelectionPlan', + 'SelectionPlan', '_Combined', }.contains(row['name'])) { classes[int.parse((row['id'] as String).split('/').last)] = diff --git a/tool/test_flutter.dart b/tool/test_flutter.dart index 030c751a..5b39cc30 100644 --- a/tool/test_flutter.dart +++ b/tool/test_flutter.dart @@ -160,8 +160,7 @@ Future main(List args) async { '-p', ], stdoutEncoding: null); if (screenshot.exitCode != 0) throw StateError('${screenshot.stderr}'); - await Directory('research/validation').create(recursive: true); - final screenshotFile = File('research/validation/flutter-android.png'); + final screenshotFile = File('.dart_tool/flutter/flutter-android.png'); await screenshotFile.writeAsBytes(screenshot.stdout as List); final sources = {}; for (final directory in [ @@ -234,10 +233,9 @@ Future main(List args) async { 'Force-stop/relaunch is process persistence evidence, not power-loss testing.', ], }; - await Directory('research/validation').create(recursive: true); - await File('research/validation/flutter.json') + await File('.dart_tool/flutter/flutter.json') .writeAsString('${const JsonEncoder.withIndent(' ').convert(result)}\n'); stdout.writeln( - 'Report: research/validation/flutter.json; screenshot: ${screenshotFile.path}', + 'Report: .dart_tool/flutter/flutter.json; screenshot: ${screenshotFile.path}', ); }