Problem/Motivation
PostgreSQL supports specialized index types, such as GIN (Generalized Inverted Index) and GiST (Generalized Search Tree), that can dramatically speed up queries on JSON columns, arrays, and full-text data. Other database engines supported by Drupal core do not offer these index types.
The Schema API can only describe regular (B-tree) indexes. There is no way for a module to request a GIN or GiST index on PostgreSQL while remaining compatible with MySQL/MariaDB and SQLite, so PostgreSQL sites cannot benefit from these index types through the standard schema definition workflow.
A concrete example is #2988018: [PP-1] Performance issues with path alias generated queries on PostgreSQL, where path alias lookups take multiple seconds on larger sites because the LIKE/ILIKE queries on the path_alias table cannot use a regular index; a GIN or GiST index would solve this. GIN index support is also needed for #3343634: Add 'json' as core data type to Schema and Database API, since on PostgreSQL jsonb columns need a GIN index to be queried efficiently.
Proposed resolution
Extend the object-oriented schema definition API (from #3113560) with specialized index definitions that drivers can support, skip, or fall back from:
- Introduce the abstract base class
\Drupal\Core\Database\SchemaDefinition\IndexBase. The existingIndexdefinition now extends it, and concrete subclasses describe the index technology to be used. - Add two specialized index definitions to the PostgreSQL driver:
\Drupal\pgsql\Driver\Database\pgsql\GinIndexand\Drupal\pgsql\Driver\Database\pgsql\GistIndex. - Add the
\Drupal\Core\Database\SchemaDefinition\AlternativeIndexesvalue object, which lists alternative definitions for the same logical index in order of preference. The driver creates the first index it supports and ignores the others, so a GIN index on PostgreSQL can fall back to a regular index on other databases. - Each driver declares the index definition classes it supports via the new protected method
Schema::getSupportedIndexDefinitionClasses(), and creates specialized indexes via the new protected methodSchema::createSpecializedIndex(). Index definitions of a technology not supported by the driver are skipped. - Accept
IndexBaseandAlternativeIndexesobjects in theindexesproperty ofTabledefinitions, and in theindexeselement of the keys specification passed toSchema::addField()andSchema::changeField()on all core drivers (MySQL/MariaDB, PostgreSQL, SQLite). The new protected helperSchema::prepareKeysSpecification()converts the definition objects and collects the specialized indexes to create. - Add the public methods
addGinIndex()andaddGistIndex()to the PostgreSQL driver'sSchemaclass to add a specialized index to an existing table directly. - GIN and GiST indexes have no operator class for character type columns (
text,character varying,character). For these columns the driver creates an expression index on the text search vector of the column, usingto_tsvector(). The text search configuration matching the site default language is used when it exists in thepg_ts_configcatalog; otherwise theenglishconfiguration is used. Queries must repeat the same expression to use the index.
Example, from a table schema definition:
indexes: [
new AlternativeIndexes(
name: 'ages',
indexes: [
new GinIndex(name: 'ages', columns: ['age']),
new Index(name: 'ages', columns: ['age']),
],
),
],
An earlier approach wrapped legacy array-based index specifications in a value object implementing ArrayAccess and Traversable to carry driver-specific configuration. This was superseded by building on the object-oriented schema definition API instead.
Remaining tasks
- Code review of the merge request.
- Update the change record (#3409274) to match the current implementation.
User interface changes
None.
API changes
- New abstract class
\Drupal\Core\Database\SchemaDefinition\IndexBase;Indexnow extends it. The index name moved to the base class constructor. - New final class
\Drupal\Core\Database\SchemaDefinition\AlternativeIndexes. - New PostgreSQL driver classes
GinIndexandGistIndex, and new public methodsSchema::addGinIndex()andSchema::addGistIndex(). - New protected methods on
\Drupal\Core\Database\Schemafor driver authors:getSupportedIndexDefinitionClasses(),createSpecializedIndex(),resolveIndexDefinition(), andprepareKeysSpecification(). - The
indexesproperty ofTabledefinitions and theindexeselement of the keys specification ofSchema::addField()andSchema::changeField()now also accept index definition objects. Existing array-based specifications keep working unchanged.
Data model changes
None.
Release notes snippet
Table schema definitions can now describe specialized index types, starting with PostgreSQL's GIN and GiST indexes, including fallback indexes for database engines that do not support them.
Issue fork drupal-3397622
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
daffie commentedFor the bikeshedding part see #2988018: [PP-1] Performance issues with path alias generated queries on PostgreSQL
Comment #3
bradjones1Minor typo fix in IS and title.
Comment #4
bradjones1Comment #5
bradjones1A bit of rubber-ducking on the possibility of using an object implementing
ArrayAccessfor a value object that would allow for additional data on the index (beyond just an array of keys.) As proposed by @alexpott.If I understand this properly, the idea is to be able to specify indexes something like (pseudocode):
If
DbIndexSpecimplements bothArrayAccessandTraversable, then it could be crafted in a BC-compatible way for any instances where it is foreach()'ed over currently. There is an instance of the Postgres driver doing a typecheck with is_array(), which would not be compatible here, but that code would be changed anyway because this is the whole reason for this conversation.Setting the
is_array()issue aside, the value object would need to be BC-compatible for nested foreach loops such as found in the mysql driver:That said, since there are contrib DB drivers out there, is this just "too much" to try and attempt?
Comment #6
bradjones1Got some feedback and background in Slack on "ArrayPI" - the name appears to be a Drupalism (and a fun one at that). There are similar discussions around trying to chip away at particularly complex or DX-unfriendly structures, e.g. #3380145-19: ViewsData should not cache by language. Seems @catch is optimistic about being able to refactor some of these with limited BC impact.
While poking around the related code I also discovered that an index's "key column specifier" can either be an array of column names OR arrays which contain a column name and prefix length. Database drivers can elect to respect the prefix or just use the full column value. (IOW, they must support the syntax but not necessarily create a functional index as a result.) I _think_ this potentially opens us up to using this array syntax to also express driver-specific options, e.g. index type. I don't love it, but it is potentially better than the BC concerns around a value object.
So we could rephrase that docblock to say something like:
This shouldn't BC-break any code that accesses the numeric-indexed members.
Comment #7
bradjones1Comment #9
bradjones1So I _think_ I might have found a way to implement a value object for the index specification that will allow for this kind of configuration to be conveyed. The only issue would be if a non-core database driver does an explicit
is_array()check on the index definitions. This would only pass that kind of very strict (and not very useful) type checking if it changed tois_iterable(), which the value object would pass.Given the talk in Slack and on related issues on this subject, I think that should be OK? And we really, really don't seem to have any other way to include this outside of sticking it in a very awkward new schema definition key like
indexes_configor something, which is not very ergonomic and shards this information from the index definitions.Comment #10
bradjones1Comment #11
bradjones1I've added a draft CR to help explain how this works and start the conversation on how to message this, since it's the first time we're doing a value object inside ArrayPI.
I think it could be interesting to encapsulate this into the coding standards re:
is_array()isis_iterable()on type checks for ArrayPI members.Comment #12
mondrakeA comment on the general approach here if I may.
I think the idea of having value objects to represent DB objects is a great step forward. Doctrine DBAL has done something similar and dropped usage of arrays to describe DB structures.
That said, and given this will set a precedent in Drupal DB API, I would suggest:
1. To have a
Schemasubfolder where to store both the value object and the enum.2. With that, the value object name could easily be
Indexinstead ofIndexSpecification.3. Why final? The value object could be extended by drivers exactly to cover DB specific features like here.
4. Testing of DB specifics should be done in the driver extensions of DriverSpecificSchemaTestBase, not in the base class.
Comment #13
bradjones1Thanks for the vote of confidence and feedback. On points 1-3 that's the kind of feedback I needed because there is no existing pattern to follow so I was just winging it.
As for #4, I do think that we need to have testing of this in the base class, to detect compatibility breaks. Basically, we need to ensure that all drivers work when they don't necessarily support the extra data and successfully just treat it as an array. Perhaps annotating the test to state as such could make it clearer why it's there. The alternative would be having a base test that has just the object and the pgsql version has the pgsql-specific semantics.
Comment #14
bradjones1I realize I missed addressing the question about making the value object
final. My justification is that the index definition object itself is driver-agnostic; it's the configuration that is specific to each flavor. So there is no legitimate reason to fork this off asPostgresIndexor whatever because it's not a postgres index - it's an index definition with some postgres metadata attached.Comment #15
mondrakeSee #3410312-14: Flood database backend ::isAllowed() should call ::ensureTableExists()
Comment #16
daffie commented@bradjones1: The solution looks good to me.
We are missing some CRUD operations with the GIN/GIST indexes. We should also add functionality and testing to:
- to check that the GIN/GIST index exists;
- to add GIN/GIST index is created on an existing field;
- to remove a GIN/GIST index;
- when we change an existing field we can also add/remove a GIN/GIST index.
Add some testing that when we create a new table with a GIN/GIST index that the index exists and is a GIN/GIST index. Please create a query with the explain to check that we have created a GIN/GIST index and that the query is using it.
Comment #17
bradjones1Rebased.
Re: The requests in #16 -
GIN/GIST indexes are cataloged by Postgres similar to other indexes, so checking they exist is already possible. To determine their type, you must dig a little deeper, so there is now an additional column of information about indexes (using
pg_get_indexdef()) which we're also using in the new tests.Test coverage added.
I'm not sure we need to do this, and if we do, this amounts to a new feature for the driver/DBAL overall and nothing specific to GIN/GIST. There is a lot of logic already in
::changeField()but none of it currently touches indexes. In that respect GIN/GiST aren't special, and I think it's incumbent upon the caller to determine if the existing indexes will be compatible. I suggest addressing this in a separate issue. (GIN/GIST do introduce some interesting new considerations around index type support for changing field types, but I still don't think that means we must build a new feature into::changeField()to get support in.)This is in the test coverage. Note that we are in a cart-and-horse situation with respect to testing GIN - its test is almost identical to GIST, we just need a supported field type to test with. So I put in a skipped test as a placeholder and once we commit this, we can enable that test coverage with/after JSON data types go in.
Comment #18
daffie commentedThe testbot is failing on PostgreSQL on your added code.
The CR needs examples on how the use the added GIN and GIST indexes.
Comment #19
bradjones1Ah, how embarassing, I forget you need to manually trigger the PgSQL tests. Fixed with some updated coverage of the introspection method.
Updating the CR now.
Comment #20
bradjones1Missed that there are also new MR comments.
Comment #21
poker10 commentedThanks for great work @bradjones1! I have added some other comments to the MR.
Comment #22
mondrakeSome comments inline.
Comment #23
bradjones1Replied to MR comments and made a few changes as a result. Back to NR.
Comment #24
mradcliffeI think this pretty good to commit. Everything makes sense based on the changes from daffie's review.
I updated the change record.
I think we can update the issue summary with the proposed resolution now. Maybe moving some of the initial resolution into the problem/motivation?
Comment #25
bradjones1Comment #26
bradjones1Comment #27
bradjones1@mradcliffe would you mind RTBC'ing per your review?
Comment #28
mradcliffeAgreed. We'll see if we need to do anything else :-)
Comment #29
quietone commentedI read the Issue Summary, the CR and then the MR. I reviewed the comments and the MR. I left comments in the MR and suggestions. At least one of the suggestions is incomplete, that is it still needs work.
The change record reads well but I was surprised to find that new enum is only mentioned at the end. I think that is important information and should be at the start. There is a code example for the 'after' situation. Can one be added for the 'before' case? And I don't follow this sentence, "The only backwards-compatibility break would be if a driver is explicitly checking the index definition with is_array(), which is currently unnecessary.' It is not clear to me when this is unnecessary, is it before this change or after. Maybe it is better to change this to something like this, "Sites that are using a driver that is explicitly checking the index definition with is_array() will need to ....'..
Comment #30
larowlanComment #31
larowlanComment #34
daffie commentedI have reviewed the MR and it looks great.
@steinmb Could you address the open remarks on the MR?
Added #3411490: Replace array-based DB Schema API with a value object structure as a related issue. Should not block this issue.
Comment #37
mondrakeNew MR!13850 is built on top of #3411490: Replace array-based DB Schema API with a value object structure, showing how we could address this issue with the feature contained there.
Comment #38
mondrakeThe new MR is green both on MySql and PgSql.
Comment #39
smustgrave commentedNot sure I'm a good person to review just bumping priority so fingers crossed 11.4!
Comment #41
smustgrave commentedAppears to need a rebase now. Going to post in core-development to try and help get eyes
Comment #43
mondrakeI no longer believe that MR!13850 is the right way to go, assuming that #3411490: Replace array-based DB Schema API with a value object structure finally gets in. In that scenario, I think we should strive to make the new index types an abstract definition, too.
However, these would only be possible to be concretely implemented by PostgreSql, at the moment. This means we need to manage a fallback situation where the abstract definition could not be fulfilled, with two possibilities:
a) do nothing if a GIN or GiST index cannot be created - this could happen if we are not interested in having an alternative index created to support data querying;
b) define an alternative index type to be created if a GIN or GiST index cannot be created - this could happen if we still want an index with a different technology created if we cannot creat the preferred one.
My current thinking:
GinIndexandGiSTIndexindexesarray property:when a db that cannot support them encounters the definition, just skips it.
AlternativeIndexesthat lists in order of preference the option for index creationindexesarray property:when the definition is read by the db driver, if the GinIndex is supported it is created, if not the Index is created instead.
Comment #44
daffie commented#3411490: Replace array-based DB Schema API with a value object structure has landed.
Comment #45
daffie commentedComment #49
daffie commentedUpdated the Is and the CR.
Comment #50
daffie commentedThe CI pipeline failures are not related to the changes in the PR.
To fix #2988018: [PP-1] Performance issues with path alias generated queries on PostgreSQL , we shall need this issue and #3586688: Add support for generated columns to the DB Schema Definition API.
Comment #51
daffie commentedComment #52
daffie commentedReady for a review.
Comment #53
mondrakeWow, quite a lot of work @daffie! I could not look at details and will be some time before I can get to it, but from the top of it I have a very fundamental comment, see comments inline - IMO we should be consistent in SchemaDefinition being immutable and database-abstract, so the new index classes should be added to SchemaDefinition and not
onlyto the PgSql driver.Comment #54
andrew.wang commentedComment #55
daffie commentedI get why you would like to have the SchemaDefinition be immutable. The problem for me is that when you start using/supporting PostgreSQL. The immutable SchemaDefinition is a bit limiting. Fro instance, I am working on a new module https://www.drupal.org/project/search_api_paradedb. The module gives a SOLR like search experience, including AI search and speed, only the search indexes are stored in PostgreSQL. No extra search server needed. Just like Drupal has module, PostgreSQL has extensions. This module uses a PostgreSQL extension which adds a new index (BM25). For a complete fun fact, there are multiple extensions that all create their own BM25 index, each with different kinds of functionality.
My question is do we want to support that in out immutable SchemaDefinition or not? And if we do want to support that, then how? I see 3 possible options:
- We do not support the new BM25 index in the SchemaDefinition;
- We support the new BM25 index in Drupal core in the namespace Drupal\Core\Database\SchemaDefinition. The SchemaDefiniton will remain ummutable.
- We let (contrib) modules extend the SchemaDefinition. Only now will the SchemaDefinition be no longer be ummutable.
@mondrake: What is your take on this?
Comment #56
mondrakeIn the most recent commits I worked on finding a better way to manage column addition/change to existing tables. I think this needs an issue of its own now, so I filed #3620121: Introduce SchemaDefinition-based alternatives to Schema::addField and ::changeField.
@daffie I saw #55, will come back on that later.
Comment #57
mondrake