Skip to content

fix: ORM log safety, @name in strings, TLS warning - #24

Merged
necessarylion merged 11 commits into
mainfrom
todo/orm-batch2
Nov 1, 2026
Merged

necessarylion merged 11 commits into
mainfrom
todo/orm-batch2

Conversation

@necessarylion

@necessarylion necessarylion commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Eight small fixes in dox_orm. One commit per item.

No breaking changes. The old behaviour stays the default. New options are opt-in. All new API is added, nothing is removed or renamed.

1. Warning when Postgres TLS does not check the certificate

useSSL: true uses TLS mode require. It encrypts the data, but it does not check the server certificate. The default does not change. Now the app prints one warning to stderr (once per isolate) when it connects with require. The warning says to set extra['sslMode'] = 'verify-full'. Set extra['sslWarning'] = false (or sslWarning=false in the url) to turn it off. It prints once per isolate: isolates share no memory, so once per process needs a shared file or similar, which is too much for a warning. The doc comments of useSSL and postgresSslMode say the same.

2. Opt-in redaction in the debug query log

The debug log prints every bound value as plain text, like password hashes and tokens. New: ConnectionConfig.logRedact (also SqlQueryBuilder().logRedact). It is a list of names, like ['password', 'token']. A value whose key ends with one of the names is shown as ***. The default is an empty list, so the log does not change unless you set it. It also works with a DATABASE_URL config.

3. Doc warning on whereRaw and orWhereRaw

The doc comments now say: never put user input in the raw text, pass values as @name params. Same as orderByRaw.

4. @name inside strings and comments on MySQL and SQLite

MySQL and SQLite changed every @name to ?, even inside a quoted string or a comment. Then the query had the wrong number of values and failed. Now the replacement skips '...', "...", `...`, --, /* */ and (MySQL) # comments, like the Postgres driver. On MySQL a \ escapes a quote in a string, and -- is a comment only when a space follows. Unknown @word (like @@sql_mode) is still kept as it is. MySQL /*! ... */ and /*M! ... */ blocks are run by MySQL, so @name inside them is still replaced, as in 3.0.1. A server with NO_BACKSLASH_ESCAPES is not handled (doc note).

5. Query log value order

The log listed values in the order they were added to the map. So where(...).update(...) showed the WHERE value first. Now the values follow the order of the ? in the query. Values that the query does not use come last. Only the log changes.

6. Opt-in buffered FileQueryPrinter

It opens, writes and closes the file on every query. This stays the default: each line is in the file before log() returns, and write errors are thrown, as in 3.0.1. New opt-in: FileQueryPrinter(file: f, buffered: true) keeps the lines in memory and appends them in the background, in order. flush() waits until every line is in the file (eg. before exit()). In buffered mode a write error prints to stderr.

7. Doc note on hasOne with many rows

When an owner has many child rows, eager loading keeps the last row. Which row is picked does not change. The docs now say to add an orderBy in onQuery to pick the row.

8. Logger.warn colour

It printed blue (34m). Its doc says yellow. It now prints yellow (33m).

Not done

toSnakeCase (Address2 to address_2) is skipped. Changing it would rename columns, so it is breaking.

Tests

  • New: test/tls_warning_test.dart, test/named_params_test.dart, test/query_log_test.dart, and one test in test/where_clauses_test.dart that runs a whereRaw with @name in a string and in comments on the real database.
  • Each new test fails on the old code (checked).
  • dart format and dart analyze --fatal-infos packages: clean.
  • dox-orm: SQLite 408 passed, Postgres 16 405 passed, MariaDB 11 405 passed.
  • dox-migration (Postgres + MariaDB): 69 passed.

useSSL: true maps to sslMode require. It encrypts the data but does not
check the server certificate. The default stays the same. The driver now
prints one warning to stderr, and the docs say to use verify-full.
The debug log prints every bound value as plain text, like password
hashes and tokens. ConnectionConfig.logRedact takes a list of names.
A value whose key ends with one of them is shown as ***. The default
is an empty list, so the log output does not change.
The text of whereRaw and orWhereRaw goes into the SQL as it is. The doc
comments now say to never put user input in it and to use @name params,
the same as orderByRaw.
MySQL and SQLite changed every @name to a ? parameter, even inside a
quoted string or a comment. Then the query had the wrong number of
values and failed. Now the replacement skips quoted strings, quoted
names and comments, like the Postgres driver does.
The log printed values in the order they were added to the map. So
where(...).update(...) showed the WHERE value before the SET values.
Now the log lists them in the order of the ? in the query. Only the
log changes, the query is the same.
FileQueryPrinter opened, wrote and closed the log file on every query,
and the query waited for the disk. Now it keeps the lines in memory and
appends them in one async write, in order. flush() waits until every
line is in the file, so no line is lost before exit().
When an owner has many child rows, hasOne eager loading keeps the last
row the query returns. The database does not promise an order. The docs
now say to add an orderBy in onQuery to pick the row. The behaviour does
not change.
Logger.warn printed blue (34m), but its doc comment says yellow. It now
uses the yellow colour code (33m).
@necessarylion

Copy link
Copy Markdown
Member Author

Bot review

Verdict: CHANGES NEEDED

  1. packages/dox-orm/lib/src/printers/file_printer.dart:44 (blocking). In 3.0.1, FileQueryPrinter.log() wrote each line to the file before it returned. Now the line is only kept in memory and written later. An app that calls exit() right after its queries loses those lines. I checked this: a script that logs 3 queries and then calls exit(0) writes 3 lines on 3.0.1 and 0 lines on this branch. Code that reads the log file right after a query (for example an app test) also sees nothing now. Write errors also changed: 3.0.1 threw, now they only go to stderr (line 64). This breaks the owner rule, and flush() does not help old apps because they do not call it.

    • Fix: keep the sync write as the default. Make the buffered write opt-in, for example FileQueryPrinter(file: f, buffered: true). Then flush() only matters for people who turn it on. Dart has no exit hook, so a "sync write on exit" is not possible. Update the CHANGELOG line and the test to use the new flag.
  2. packages/dox-orm/lib/src/drivers/db_driver.dart:297 (small, please fix). In MySQL mode, /*! ... */ and MariaDB /*M! ... */ are not comments. MySQL runs the SQL inside them. 3.0.1 replaced a @name inside them, and that worked. Now it is skipped, so such a query fails. This is rare, but it is a change for a 3.0.1 app.

    • Fix: when mysql is true and the char after /* is ! (or M!), do not skip that block. Add one test.
  3. packages/dox-orm/lib/src/drivers/db_driver.dart:280 (note, no change needed). A MySQL server with sql_mode NO_BACKSLASH_ESCAPES does not treat \ as an escape. So the parser can read the end of a string like 'C:\' in the wrong place. A @name after it is then skipped. The default sql_mode works correctly, so I accept this. Please add one line about it in the doc comment.

  4. packages/dox-orm/lib/src/drivers/postgres_native_driver.dart:12 (note, no change needed). _tlsWarned is a top-level variable, so the warning prints once per isolate, not once per process. An app that runs one isolate per core prints it N times at start. It also prints when the user set sslmode=require on purpose (common in hosted Postgres URLs), and there is no way to turn it off. It goes only to stderr and does not change behaviour, so it is not a break. If it gets noisy, add an opt-out, or warn only when require comes from useSSL and not from an explicit sslMode.

  5. packages/dox-orm/lib/src/utils/logger.dart:16 (note). The log always parses with the SQLite rules (mysql: false). A MySQL query with \' in a string can show a wrong log line. Only the log changes. Fine as it is.

What I checked and found OK

  • @name change. No 3.0.1 query can depend on @name being replaced inside a string, a backtick name or a comment. On 3.0.1 that always sends more values than real ?, and both drivers reject it. I ran the new where_clauses test on v3.0.1 code. MariaDB failed with "the statement takes 2 parameters but 6 were given". SQLite failed with "Expected 2 parameters, got 6". The \ escape is right for the default MySQL mode. '' works. -- needs a space (or any char <= 0x20) in MySQL mode, like MySQL. # is a comment only for MySQL. An apostrophe inside a --, # or /* */ comment does not open a string. -- with no space (MySQL) was not a comment before either, so it still works. The query builder never makes SQL with a stray quote, -- or #.
  • logRedact. The default is an empty list, and _hidden returns false right away. Values print exactly as before, except for the new ? order (item 6 of the PR, a log-only fix).
  • whereRaw, hasOne docs, Logger.warn yellow. These are docs and a color fix. They are OK.
  • Public API. It only adds things: new named params with defaults, and SqlParam/findSqlParams are hidden from the export.

Do the tests fail without the fix?

  • where_clauses @name test with the old db_driver.dart: fails on SQLite and MariaDB.
  • query_log_test with the old logger.dart: the 2 order tests, the redaction test and the yellow warn test fail.
  • named_params_test, tls_warning_test and the FileQueryPrinter test use new API, so they do not compile on the old code.

Results

  • dart analyze: no issues.
  • SQLite: all tests passed (406).
  • Postgres 16 (Docker): all tests passed (403, 4 skipped).
  • MariaDB 11 (Docker): all tests passed (403, 4 skipped).
  • gh pr checks 24: lint pass, test pass.

The background write changed what 3.0.1 apps get: lines were lost on
exit(), a file read right after a query was empty, and write errors
were no longer thrown. The sync write is the default again. The
background write is now opt-in with buffered: true, plus flush().
MySQL runs the SQL inside /*! ... */ and /*M! ... */, so they are not
comments. 3.0.1 replaced @name inside them. The new parser skipped them,
so such a query failed. In MySQL mode they are now read as normal SQL.
The doc also says that NO_BACKSLASH_ESCAPES is not handled.
Apps that use sslmode require on purpose had no way to hide the
warning. extra['sslWarning'] = false (or sslWarning=false in the url)
now turns it off. The doc says it prints once per isolate, because
isolates share no memory.
@necessarylion

Copy link
Copy Markdown
Member Author

Fixer reply

Fixed all points. Three new commits on the same branch.

  1. FileQueryPrinter (blocking). The sync write is the default again. Each line is in the file before log() returns, and write errors are thrown, as in 3.0.1. The background write is now opt-in: FileQueryPrinter(file: f, buffered: true) with flush(). New test: log two lines, then read the file at once with no flush (default mode). The CHANGELOG line is updated. Commit fix: keep the sync write as the FileQueryPrinter default.
  2. MySQL /*! */ blocks. In MySQL mode /*! ... */, /*!50100 ... */ and /*M! ... */ are read as normal SQL, so @name inside them is replaced, as in 3.0.1. Outside MySQL mode they are still comments. New test in named_params_test.dart. Commit fix: bind @name inside MySQL /*! */ blocks again.
  3. NO_BACKSLASH_ESCAPES. I added a doc note on findSqlParams.
  4. TLS warning. extra['sslWarning'] = false (or sslWarning=false in the database url) turns it off. It still prints once per isolate. Isolates share no memory, so once per process would need a shared file or something like it. That is too much for one stderr line. The doc says so. Commit feat: let apps turn off the Postgres TLS warning.
  5. Log parse rules. No change, as you said.

Still no breaking changes.

Results

  • dart format and dart analyze --fatal-infos packages: clean.
  • dox-orm: SQLite 408 passed, Postgres 16 405 passed, MariaDB 11 405 passed (Docker).
  • dox-migration (Postgres + MariaDB): 69 passed.

@necessarylion

Copy link
Copy Markdown
Member Author

Bot review

Verdict: OK

  1. FileQueryPrinter sync write is the default again. Fixed. With no flag, log() opens the file, appends the line and closes it before it returns, as in 3.0.1. I checked it: a script logs 3 queries and calls exit(0), and all 3 lines are in the file. A write error still throws (FileSystemException). The background write only runs with buffered: true. One small difference: the sync path now creates a missing parent folder. In 3.0.1 that case threw. This only makes a failing case work, so it is not a break.
  2. @name inside MySQL /*! */ blocks is replaced again. Fixed. In MySQL mode /*!50100 @a */ and /*M! @b */ give a and b. A normal /* @c */ is still skipped. Outside MySQL mode /*! */ is still a comment. A new test covers it.
  3. The TLS warning can be turned off. Fixed. extra['sslWarning'] = false turns it off. sslWarning=false in the url also works, because the url parser turns false into a bool. Note only: a value like sslWarning=0 becomes an int and the as bool? cast throws. verifyCertificate already works the same way, so this is fine.
  4. NO_BACKSLASH_ESCAPES note. The doc line is added on findSqlParams. OK.
  5. Final pass against v3.0.1. I read the full diff of packages/dox-orm/lib. In default use nothing changes, except:
    • the model with no fillable throws (PR fix: ORM safety fixes [todo:S4,B1,S5] #23, accepted),
    • the query log shows values in the right ? order (intended, log only),
    • Logger.warn prints yellow, not blue (a color fix),
    • a Postgres connection with sslMode require prints one stderr line (can be turned off).
      The public API only adds things: new named params with defaults and MassAssignmentException. SqlParam and findSqlParams stay hidden from the export. No breaking change.

Results

  • dart format: no changes. dart analyze --fatal-infos packages: no issues.
  • SQLite: all tests passed (408).
  • Postgres 16 (Docker): all tests passed (405, 4 skipped).
  • MariaDB 11 (Docker): all tests passed (405, 4 skipped).
  • gh pr checks 24: lint pass, test pass.

@necessarylion
necessarylion merged commit 01c636a into main Nov 1, 2026
2 checks passed
@necessarylion
necessarylion deleted the todo/orm-batch2 branch November 1, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant