Add first-class JSON columns and fix the SQLite test run - #2
Merged
Merged
Conversation
JSON columns (MySQL/MariaDB JSON, PostgreSQL json and jsonb, SQLite JSON)
were mapped to STRING: reads returned raw text, assigning an array stored
"Array", and to_json() double-encoded the document. They now become
Column::JSON and hold an ActiveRecord\Json value object.
- Json implements ArrayAccess, Countable, IteratorAggregate and
JsonSerializable. Nested elements are returned by reference so writes
like $doc->payload['tags'][] = 'x' land in the document; top-level
writes flag the model dirty at once and in-place nested changes are
detected on save() by comparing against a clean snapshot. A top-level
{} survives the round trip; reading a missing key does not alter the
document.
- Column::cast() parses strings as JSON text, so code that assigned
json_encode()'d strings keeps working, and wraps arrays, scalars and
objects. cast_default() keeps expression defaults from breaking
metadata loading.
- Model casts objects for JSON columns, copies a document assigned from
another model or attribute, gives each new record its own copy of a
JSON default, and folds document changes into is_dirty(),
dirty_attributes() and attribute_is_dirty(). Table encodes documents
on insert and update.
- static $json_attributes on a model opts text columns into the same
handling.
- The MySQL adapter detects MariaDB JSON columns, which are reported as
longtext, through their json_valid() check constraints.
- to_json() and to_array() nest documents; XML writes them as elements
and CSV as JSON text.
The suite also passes with SQLite as the default adapter now:
- The SQLite adapter only claims ORDER BY/LIMIT support on UPDATE and
DELETE when the build has SQLITE_ENABLE_UPDATE_DELETE_LIMIT.
- Connection::query() binds values with a PDO type matching the PHP
type. execute() bound everything as text, and SQLite does not convert
text back to a number against an expression with no affinity, so an
association condition like "length(title) = ?" never matched.
- DatabaseTest rebuilds the SQLite schema when a previous class deleted
the file, and two tests that assumed a network URL or a database that
rejects IN() with no values handle SQLite explicitly.
New tests: JsonTest covers the value object and casts without a
database; MysqlJsonTest, PgsqlJsonTest and SqliteJsonTest run the same
model-level cases against each database through a new documents table.
Verified on MySQL 5.7 and 8.4, MariaDB 11.8, PostgreSQL 18 and SQLite.
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.
Summary
JSON columns (MySQL/MariaDB
JSON, PostgreSQLjson/jsonb, SQLiteJSON) were mapped toSTRING: reads returned raw text, assigning an array storedArray, andto_json()double-encoded the document. They now becomeColumn::JSONand hold anActiveRecord\Jsonvalue object.JsonimplementsArrayAccess,Countable,IteratorAggregateandJsonSerializable. Nested elements are returned by reference so writes like$doc->payload['tags'][] = 'x'land in the document. Top-level writes flag the model dirty at once; nested in-place changes are detected onsave()against a clean snapshot. A top-level{}survives the round trip, and reading a missing key does not alter the document.Column::cast()parses strings as JSON text, so existing code that assignedjson_encode()'d strings keeps working, and wraps arrays, scalars and objects.cast_default()keeps expression defaults from breaking metadata loading.Modelcasts objects for JSON columns, copies a document assigned from another model or attribute, gives each new record its own copy of a JSON default, and folds document changes intois_dirty(),dirty_attributes()andattribute_is_dirty().Tableencodes documents on insert and update.static $json_attributeson a model opts text columns into the same handling.longtext, through theirjson_valid()check constraints.to_json()andto_array()nest documents; XML writes them as elements and CSV as JSON text.SQLite as the default adapter
The suite now passes with
PHPAR_ADAPTER=sqlite(master had 147 errors and 2 failures):ORDER BY/LIMITsupport onUPDATE/DELETEwhen the build hasSQLITE_ENABLE_UPDATE_DELETE_LIMIT. Debian/Ubuntu builds (CI) have it; macOS does not, so those two tests skip there.Connection::query()binds values with a PDO type matching the PHP type.execute()bound everything as text, and SQLite does not convert text back to a number against an expression with no affinity, so an association condition likelength(title) = ?never matched. Booleans deliberately stay text so PostgreSQL keeps acceptingtruefor integer columns.DatabaseTestrebuilds the SQLite schema when a previous class deleted the file. Two tests that assumed a network URL, or a database that rejectsIN()with no values, handle SQLite explicitly.Known limits
[]; only the top-level{}is preserved.Tests
test/JsonTest.phpcovers the value object and casts without a database.MysqlJsonTest,PgsqlJsonTestandSqliteJsonTestrun the same model-level cases (test/helpers/JsonModelTestCase.php) against each database in every job through a newdocumentstable, independently of the default adapter.Verified locally: 911 tests green on MySQL 5.7 and 8.4, MariaDB 11.8, PostgreSQL 18 and SQLite, with each of mysql, pgsql and sqlite as the default adapter, and phpcs clean.