Conversation
…every driver INSERT INTO t () VALUES () is a MySQL-only extension, so insert([]) failed with a syntax error on PostgreSQL, SQLite and MS SQL. Those engines use the standard DEFAULT VALUES clause instead, which MySQL in turn does not know, so there is no common syntax and the driver has to decide.
… multi-insert (nette#330) Multi-line inserting wasn't returning the number of affected rows but an ActiveRow instead (with the first inserted record). The documented behaviour is: a single associative array inserts one row and returns the ActiveRow, a list of rows or a Selection performs a bulk insert and returns the count. A list is recognized by its keys being integers - they need not be sequential, since a filtered array leaves gaps in them, and an integer is never a column name. Such rows are reindexed before the query. Rows coming from a Traversable are drained by position, so none is lost when a generator yields them under colliding keys (`yield from`). An empty array stays a single insert of database defaults, so an unfilled form ($form->getValues()) still inserts a row. Both checks live in Helpers::materializeRows() and Helpers::isRowList(), so that insert() and GroupedSelection share one notion of what a list of rows is. forum thread: https://forum.nette.org/cs/36954-nette-database-v3-2-9-phpdoc-selection-insert-nepokryva-dokumentovany-bulk-insert Co-authored-by: Matěj Kmínek <m@kminet.eu>
…method The three identical unset() of refCache['referencing'] in insert()/insertMany() are replaced by a single clearReferencingCache() method.
…tifiable row The composite-primary-key branch that returns null skipped clearReferencingCache(), unlike the other two null-returning branches. Since a row was inserted but not added to $this->rows, a cached referencing (grouped) selection would stay stale. All three branches now invalidate consistently.
…eters of discarded conditions
…alse for binary (non-UTF-8) parameters and made condition dedup collide
… a WHERE identical to an existing JOIN condition is no longer dropped
…tly mutate the builder with the implicit ORDER BY
…arent rows and no longer lost from the builder after execution
…key, a re-query used to crash with LogicException
…ception instead of TypeError
… the final ROLLBACK fails (e.g. after a deadlock)
…tead of a raw class-not-found Error
…s, SQLite CHECK, PostgreSQL 57P0x and MySQL 4031 as connection lost)
…-only and zero-limit queries
… column names and regex meta-characters
…ng a phantom empty table on drivers that do not fail (SQLite)
…, which corrupted identifiers
…lumn order, functional index parts, view comment, per-connection column-type cache)
…ollection for queries over the cap
Assigning the referencing group wrote the grouping column straight into the rows it was given. For an array that is harmless, but a Nette\Database\Row is an object, so the column leaked back into the caller's instance. Rows are now cloned before the group is assigned to them.
No single release supports PHP 8.1 through 8.5 (5.12 covers 8.1-8.3, 5.13 covers 8.3-8.5), so the 5.12.0 pin could never install on the 8.4 and 8.5 jobs. Without a version setup-php resolves the newest release compatible with each PHP.
… RetryableException Added optional $attempts parameter. When greater than 1, any exception implementing RetryableException on the outermost transaction triggers a retry of the whole callback. The callback must be idempotent. A new RetryableException marker interface is introduced; DeadlockException, LockTimeoutException and ConnectionLostException all implement it. Applications can mark their own transient errors with the interface (e.g. optimistic lock conflicts) to opt into automatic retries. Nested transactions never retry on their own — the exception bubbles up to the outermost transaction, which honors its own $attempts setting.
ActiveRow becomes an empty shell over RowBehavior and must stay so. A row class can now compose the trait and implement the Row contract while extending a plain value base class, which enables detached, database-free row values; see the test for the userland recipe. The Row interface is deliberately minimal and meant to be implemented solely via RowBehavior, so it may gain members in minor releases.
…y type When a subclass declares a BackedEnum-typed public property, the column value is converted via Enum::from() on read (__get, toArray, iterator). Writes already work thanks to SqlPreprocessor handling BackedEnum.
…precated The delegation is reversed: insertMany() now performs the bulk insert itself (a Selection source or a list of rows), while insert() routes its bulk cases to it, triggers a deprecation notice and keeps only the single-row path - which also reduces the entity mapping there to a single translateColumns() call. Both methods share Helpers::materializeRows() and isRowList(). Rows coming from a Traversable are drained by position, so none is lost when a generator yields them under colliding keys (`yield from`), and an array with gaps left by array_filter() is accepted as well. An empty array stays a single row of database defaults, it is not a bulk insert. GroupedSelection assigns the referencing group to a copy of each row, never to the caller's Row object.
…identifiable row When a single insert cannot determine the primary key of the inserted row (composite primary key with a missing column), insert() used to return the input data array, which is meaningless to the caller. It now returns null. The return type narrows from ActiveRow|array|int to ActiveRow|int|null.
…tified A single insert into a table without a primary key (or when the primary key cannot be determined) used to return the affected-row count, which is always 1 and thus meaningless. It now returns null, like the composite-incomplete case, so the single-row return type is consistently ActiveRow|null.
…rn a lazy row or null The per-shape completeness checks are replaced by a single count comparison against the primary-key columns. insert() no longer falls back to an eager fetch: it returns a lazy row whenever the whole primary key is known (single or composite) and null otherwise. As a result a composite key whose database-generated part cannot be determined now returns null instead of being fetched eagerly.
…mn warns Columns are taken from the first row, so a later row that misses one used to be silently filled with NULL (with an "Undefined array key" warning), and a column the first row does not have was dropped without a trace. Both now warn.
Values belong in parameters. A literal written straight into a SQL fragment is not recognized, so its content gets delimited as if it were an identifier (name = 'abc' becomes [name] = '[abc]'), which silently returns wrong rows instead of failing. Whether it happens at all depends on the content: 'x' breaks, '!' does not, so the trap is impossible to guess.
max()/min()/sum() answered 0 for a group with no rows, which is a value the data never contained and which the plain Selection does not report either - it returns null there, like the SQL aggregate functions do for an empty set. count() keeps returning 0, it casts the result.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Apply the possibility to use fetchAssoc to get native array type of next row to fetchAll without the need to convert from \Nette\Database\Row back to array.
\Nette\Database\Row cannot be typed properly (at least I wasn't able to use something like
/** @var \Nette\Database\Row{id: int} */But this syntax is possible to use with fetchAssoc because that returns array:
/** @var array{id: int, name: string} $this->db->fetchAssoc("SELECT id, name FROM user WHERE id = ?", $user_id);This new proposed function fetchAllArray copies what fetchAll does but without the need to convert array that are retrieved from the driver into \Nette\Database\Row (Arrays::toObject($data, new Row)) while not breaking the iterator over rows with original type.
The result is list which can be typed similarly as example above.
/** @var list<array{id: int, name: string}> $this->db->fetchAssoc("SELECT id, name FROM user");This appoach unclocks new possibilities for static analysis. We have to keep in mind that these hints rely on developer knowledge of the query string used and will likely not be useful for higly dynamic queries where unions should have to be used and checked.
It also saves some performance because convertion to \Nette\Database\Row is not useful most of the time.