@@ -124,12 +124,9 @@ abstract class SQL extends Adapter implements Feature\Connection, Feature\RawQue
124124
125125 private const int UNPAIRED_JOINED_ROWS = 4 ;
126126
127- protected object $ pdo ;
127+ private const string ROUND_TRIP_PRECISION = ' -1 ' ;
128128
129- /**
130- * Controls how many fractional digits are used when binding float parameters.
131- */
132- protected int $ floatPrecision = 17 ;
129+ protected object $ pdo ;
133130
134131 /**
135132 * Lazily constructed AttributeMap shared by every builder's scope.
@@ -209,11 +206,57 @@ public function getDriver(): object
209206 }
210207
211208 /**
212- * Helper to format a float value according to configured precision for binding/logging.
209+ * A decimal that reads back as exactly this double, in as few digits as PHP writes it when they suffice. Casts
210+ * and `%H` ignore the locale, and a double needs at most 17 significant digits to round-trip.
211+ */
212+ protected function formatFloat (float $ value ): string
213+ {
214+ $ formatted = (string ) $ value ;
215+ if ((float ) $ formatted === $ value ) {
216+ return $ formatted ;
217+ }
218+
219+ $ formatted = \sprintf ('%.16H ' , $ value );
220+
221+ return (float ) $ formatted === $ value ? $ formatted : \sprintf ('%.17H ' , $ value );
222+ }
223+
224+ /**
225+ * Binds a value as its PDO type, and a double as its exact decimal: PDO would write it at the `precision` ini.
226+ *
227+ * @param PDOStatement|DatabasePDOStatement|PDOStatementProxy $statement
228+ */
229+ protected function bind (object $ statement , int |string $ parameter , mixed $ value ): void
230+ {
231+ if (\is_float ($ value )) {
232+ $ statement ->bindValue ($ parameter , $ this ->formatFloat ($ value ), PDO ::PARAM_STR );
233+
234+ return ;
235+ }
236+
237+ $ statement ->bindValue ($ parameter , $ value , $ this ->getPdoType ($ value ));
238+ }
239+
240+ /**
241+ * Runs a fetch at the precision that round-trips every double. PDO stringifies a double it fetches at the
242+ * `precision` ini, 14 significant digits by default.
243+ *
244+ * @template T
245+ *
246+ * @param callable(): T $fetch
247+ * @return T
213248 */
214- protected function getFloatPrecision ( float $ value ): string
249+ protected function fetchExactly ( callable $ fetch ): mixed
215250 {
216- return sprintf ('%. ' .$ this ->floatPrecision .'F ' , $ value );
251+ $ precision = \ini_set ('precision ' , self ::ROUND_TRIP_PRECISION );
252+
253+ try {
254+ return $ fetch ();
255+ } finally {
256+ if ($ precision !== false ) {
257+ \ini_set ('precision ' , $ precision );
258+ }
259+ }
217260 }
218261
219262 #[\Override]
@@ -621,12 +664,12 @@ public function getDocument(Document $collection, string $id, array $queries = [
621664 try {
622665 $ statement = $ this ->prepareStatement ($ sql , Event::DocumentRead);
623666 foreach ($ bindings as $ parameter => $ value ) {
624- $ statement -> bindValue ( $ parameter , $ value , $ this -> getPdoType ( $ value) );
667+ $ this -> bind ( $ statement , $ parameter , $ value );
625668 }
626669 $ this ->describeStatement ($ statement , $ bindings , $ name );
627670 $ this ->execute ($ statement );
628671 /** @var array<string, mixed>|false $row */
629- $ row = $ statement ->fetch (PDO ::FETCH_ASSOC );
672+ $ row = $ this -> fetchExactly ( static fn (): mixed => $ statement ->fetch (PDO ::FETCH_ASSOC ) );
630673 } catch (PDOException $ e ) {
631674 $ exception = $ e ;
632675 } finally {
@@ -984,7 +1027,7 @@ private function readRows(string $name, array $documents, array $columns): array
9841027 $ statement = $ this ->executeResult ($ builder ->build (), Event::DocumentRead);
9851028 $ this ->execute ($ statement );
9861029 /** @var list<list<mixed>> $rows */
987- $ rows = $ statement ->fetchAll (PDO ::FETCH_NUM );
1030+ $ rows = $ this -> fetchExactly ( static fn (): array => $ statement ->fetchAll (PDO ::FETCH_NUM ) );
9881031 $ statement ->closeCursor ();
9891032
9901033 return $ rows ;
@@ -1536,7 +1579,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
15361579 $ this ->describeStatement ($ statement , [], $ name );
15371580 $ this ->execute ($ statement );
15381581 /** @var array<int, array<string, mixed>> $rows */
1539- $ rows = $ statement ->fetchAll ();
1582+ $ rows = $ this -> fetchExactly ( static fn (): array => $ statement ->fetchAll () );
15401583 } catch (PDOException $ e ) {
15411584 $ exception = $ e ;
15421585 } finally {
@@ -1837,14 +1880,14 @@ public function rawQuery(string $query, array $bindings = []): array
18371880 try {
18381881 $ statement = $ this ->prepareStatement ($ query );
18391882 foreach ($ bindings as $ i => $ value ) {
1840- $ statement -> bindValue ( $ i + 1 , $ value, $ this -> getPdoType ( $ value ) );
1883+ $ this -> bind ( $ statement , $ i + 1 , $ value );
18411884 }
18421885 $ this ->execute ($ statement );
18431886 } catch (PDOException $ e ) {
18441887 throw $ this ->processException ($ e );
18451888 }
18461889
1847- $ results = $ statement ->fetchAll ();
1890+ $ results = $ this -> fetchExactly ( static fn (): array => $ statement ->fetchAll () );
18481891 $ statement ->closeCursor ();
18491892
18501893 $ documents = [];
@@ -3552,7 +3595,7 @@ public function rawMutation(string $query, array $bindings = []): int
35523595 try {
35533596 $ statement = $ this ->prepareStatement ($ query );
35543597 foreach ($ bindings as $ i => $ value ) {
3555- $ statement -> bindValue ( $ i + 1 , $ value, $ this -> getPdoType ( $ value ) );
3598+ $ this -> bind ( $ statement , $ i + 1 , $ value );
35563599 }
35573600 $ this ->execute ($ statement );
35583601 } catch (PDOException $ e ) {
@@ -3661,10 +3704,6 @@ protected function writeContext(array $skipPermissions = []): WriteContext
36613704 * Prepares the SQL statement and binds positional parameters from the Statement.
36623705 * Does NOT call execute() - the caller is responsible for that.
36633706 *
3664- * Floats are bound as 7.x bound them: a find() binds them in fixed-point notation, every other statement as
3665- * PHP writes them, so a write keeps a magnitude fixed point would round to zero. A bulk delete binds them as a
3666- * find() does, so its filters match the rows the find() of the documents it deletes matched.
3667- *
36683707 * @param string $collection The collection the statement reads or writes, for the profiler
36693708 * @return PDOStatement|DatabasePDOStatement|PDOStatementProxy
36703709 */
@@ -3676,13 +3715,7 @@ protected function executeResult(Statement $result, ?Event $event = null, string
36763715 if (\is_bool ($ value ) && $ this ->supports (Capability::IntegerBooleans)) {
36773716 $ value = (int ) $ value ;
36783717 }
3679- if (\is_float ($ value ) && ($ event === Event::DocumentFind || $ event === Event::DocumentsDelete)) {
3680- $ prepared ->bindValue ($ i + 1 , $ this ->getFloatPrecision ($ value ), PDO ::PARAM_STR );
3681- } elseif (\is_float ($ value )) {
3682- $ prepared ->bindValue ($ i + 1 , $ value , PDO ::PARAM_STR );
3683- } else {
3684- $ prepared ->bindValue ($ i + 1 , $ value , $ this ->getPdoType ($ value ));
3685- }
3718+ $ this ->bind ($ prepared , $ i + 1 , $ value );
36863719 }
36873720
36883721 return $ prepared ;
@@ -6266,7 +6299,7 @@ private function runSelect(Statement $result, Event $event, string $collection):
62666299 $ statement = $ this ->executeResult ($ result , $ event , $ collection );
62676300 $ this ->execute ($ statement );
62686301 /** @var array<int, array<string, mixed>> $results */
6269- $ results = $ statement ->fetchAll (PDO ::FETCH_ASSOC );
6302+ $ results = $ this -> fetchExactly ( static fn (): array => $ statement ->fetchAll (PDO ::FETCH_ASSOC ) );
62706303 } catch (PDOException $ e ) {
62716304 $ exception = $ e ;
62726305 } finally {
@@ -6520,34 +6553,34 @@ protected function bindOperatorParameters(object $statement, Operator $operator,
65206553 case OperatorType::Divide:
65216554 $ value = $ values [0 ] ?? 1 ;
65226555 $ bindKey = "op_ {$ bindIndex }" ;
6523- $ statement -> bindValue ( ': ' .$ bindKey , $ value, $ this -> getPdoType ( $ value ) );
6556+ $ this -> bind ( $ statement , ': ' .$ bindKey , $ value );
65246557 $ bindIndex ++;
65256558
65266559 if (isset ($ values [1 ])) {
65276560 $ limitKey = "op_ {$ bindIndex }" ;
65286561 $ limit = self ::exactLimit ($ values [1 ]);
6529- $ statement -> bindValue ( ': ' .$ limitKey , $ limit, $ this -> getPdoType ( $ limit ) );
6562+ $ this -> bind ( $ statement , ': ' .$ limitKey , $ limit );
65306563 $ bindIndex ++;
65316564 }
65326565 break ;
65336566
65346567 case OperatorType::Modulo:
65356568 $ value = $ values [0 ] ?? 1 ;
65366569 $ bindKey = "op_ {$ bindIndex }" ;
6537- $ statement -> bindValue ( ': ' .$ bindKey , $ value, $ this -> getPdoType ( $ value ) );
6570+ $ this -> bind ( $ statement , ': ' .$ bindKey , $ value );
65386571 $ bindIndex ++;
65396572 break ;
65406573
65416574 case OperatorType::Power:
65426575 $ value = $ values [0 ] ?? 1 ;
65436576 $ bindKey = "op_ {$ bindIndex }" ;
6544- $ statement -> bindValue ( ': ' .$ bindKey , $ value, $ this -> getPdoType ( $ value ) );
6577+ $ this -> bind ( $ statement , ': ' .$ bindKey , $ value );
65456578 $ bindIndex ++;
65466579
65476580 if (isset ($ values [1 ])) {
65486581 $ maxKey = "op_ {$ bindIndex }" ;
65496582 $ limit = self ::exactLimit ($ values [1 ]);
6550- $ statement -> bindValue ( ': ' .$ maxKey , $ limit, $ this -> getPdoType ( $ limit ) );
6583+ $ this -> bind ( $ statement , ': ' .$ maxKey , $ limit );
65516584 $ bindIndex ++;
65526585 }
65536586 break ;
@@ -6596,7 +6629,7 @@ protected function bindOperatorParameters(object $statement, Operator $operator,
65966629 if (is_array ($ value )) {
65976630 $ value = json_encode ($ value );
65986631 }
6599- $ statement -> bindValue ( ': ' .$ bindKey , $ value, $ this -> getPdoType ( $ value ) );
6632+ $ this -> bind ( $ statement , ': ' .$ bindKey , $ value );
66006633 $ bindIndex ++;
66016634 break ;
66026635
0 commit comments