Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<!-- Directories and third party library exclusions. -->
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
<exclude-pattern>/wordpress/*</exclude-pattern>
<exclude-pattern>/wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php</exclude-pattern>

Expand Down
253 changes: 253 additions & 0 deletions tests/WP_SQLite_Driver_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -6323,6 +6323,259 @@ public function testTransactionSavepoints(): void {
$this->assertSame( array(), (array) array_column( $result, 'id' ) );
}

public function testSelectOrderByAmbiguousColumnResolution(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );
$this->assertQuery( 'INSERT INTO t1 (id, name) VALUES (1, "A1"), (2, "A2")' );
$this->assertQuery( 'INSERT INTO t2 (id, name) VALUES (1, "B2"), (2, "B1")' );

// The "name" column will be resolved to "t1.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t1.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC' );
$this->assertEquals(
array(
(object) array( 'name' => 'A2' ),
(object) array( 'name' => 'A1' ),
),
$result
);

// The "name" column will be resolved to "t2.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t2.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC' );
$this->assertEquals(
array(
(object) array( 'name' => 'B2' ),
(object) array( 'name' => 'B1' ),
),
$result
);

// The "name" column will be resolved to "t1.name", the "id" column will be resolved to "t2.id".
$this->assertQuery( 'INSERT INTO t1 (id, name) VALUES (3, "A2")' );
$this->assertQuery( 'INSERT INTO t2 (id, name) VALUES (3, "A2")' );
$result = $this->assertQuery( 'SELECT t2.id, t1.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name, id DESC' );
$this->assertEquals(
array(
(object) array(
'id' => '1',
'name' => 'A1',
),
(object) array(
'id' => '3',
'name' => 'A2',
),
(object) array(
'id' => '2',
'name' => 'A2',
),
),
$result
);

// The "name" column will be resolved to "t1.name" in the subquery and to "t2.name" in the root query.
$result = $this->assertQuery(
'
SELECT t2.name, s.name AS subquery_name
FROM (SELECT t1.id, t1.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC LIMIT 1) s
JOIN t2 ON true
ORDER BY name DESC
'
);
$this->assertEquals(
array(
(object) array(
'name' => 'B2',
'subquery_name' => 'A2',
),
(object) array(
'name' => 'B1',
'subquery_name' => 'A2',
),
(object) array(
'name' => 'A2',
'subquery_name' => 'A2',
),
),
$result
);

// Parenthesized column reference can be used in both SELECT and ORDER BY lists.
$result = $this->assertQuery( 'SELECT (t1.name) FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY (((name))) DESC' );
$this->assertEquals(
array(
(object) array( '(t1.name)' => 'A2' ),
(object) array( '(t1.name)' => 'A2' ),
(object) array( '(t1.name)' => 'A1' ),
),
$result
);

/*
* With multiple identical aliases and no ambiguous column references,
* it just works, although sometimes the order may differ from MySQL.
* It may be nondeterministic, but it seems like MySQL picks the first
* non-column alias, while SQLite sorts by the first alias in the list.
*
* When we replace "SELECT t1.name" with "SELECT t2.name" in the query
* below, the SQLite order will differ from MySQL.
*/
$result = $this->assertQuery(
"
SELECT t1.name AS name, CONCAT(t1.name, '-one') AS name, CONCAT(t2.name, '-two') AS name
FROM t1 JOIN t2 ON t2.id = t1.id
ORDER BY name DESC
"
);
$this->assertEquals(
array(
(object) array( 'name' => 'B1-two' ),
(object) array( 'name' => 'A2-two' ),
(object) array( 'name' => 'B2-two' ),
),
$result
);

/*
* The following query fails with "ambiguous column name" in MySQL, but
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So sometimes it's MySQL that fails with "ambiguous column name" and sometimes it's SQLite? I've noticed there's a different comment that talks about SQLite failing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, both MySQL and SQLite throw that error, and they slightly differ in edge cases:

  1. SQLite doesn't use the SELECT list to try to disambiguate. That's what we're emulating.
  2. Both, interestingly, allow multiple identical explicit aliases (1 AS col, 2 AS col, 3 AS col) — but here they may slightly differ in what they would sort by with ORDER BY col.
  3. MySQL doesn't allow a special case when multiple columns share the same explicit alias. Even if these are entirely different columns (id and name, for example). SQLite is fine with that and handles it the same as in point 2.

We're not emulating anything for 2 and 3 — just passing it to SQLite "as is," because it can handle it, just with these edge case variations.

* in SQLite, it works. It's OK to keep this difference as MySQL behaves
* rather strangely in this case:
*
* 1) This is OK in MySQL:
* SELECT t1.name AS col, 123 AS col ... ORDER BY col
* 2) This fails in MySQL:
* SELECT t1.name AS col, t2.name AS col ... ORDER BY col
*/
$this->assertQuery( 'SELECT t1.name AS col, t2.name AS col FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY col' );
}
Copy link
Collaborator

@adamziel adamziel Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test syntactically tricky cases such as:

WITH temp(name) AS (
  VALUES
    ('a'),
    ('b')
)
SELECT t1.name, (SELECT name from temp WHERE id = 5) FROM t1 ORDER BY name;

WITH temp(name, b) AS (SELECT * FROM t2)
SELECT t1.name, (SELECT name from temp WHERE id = 5) FROM t1 ORDER BY name;

SELECT * FROM (
	SELECT t1.name, t2.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name
) ORDER BY col

It's fine if they fail, we need to set the line somewhere. Let's just make sure we account for them in the test suite.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel The first one doesn't seem to be a valid MySQL syntax (something about the VALUES part). The last one, we already have very similar. I'll add a test using the WITH syntax.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, the first one is SQLite


public function testSelectOrderByAmbiguousColumnError(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT t1.name, t2.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC' );
}


public function testSelectOrderByAmbiguousColumnErrorWithoutSelectList(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT 1 FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name' );
}

public function testSelectGroupByAmbiguousColumnResolution(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );
$this->assertQuery( 'INSERT INTO t1 (id, name) VALUES (1, "A"), (2, "A")' );
$this->assertQuery( 'INSERT INTO t2 (id, name) VALUES (1, "B1"), (2, "B2")' );

// The "name" column will be resolved to "t1.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t1.name FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY name' );
$this->assertEquals(
array( (object) array( 'name' => 'A' ) ),
$result
);

// The "name" column will be resolved to "t2.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t2.name FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY name' );
$this->assertEquals(
array(
(object) array( 'name' => 'B1' ),
(object) array( 'name' => 'B2' ),
),
$result
);

// Parenthesized column reference can be used in both SELECT and GROUP BY lists.
$result = $this->assertQuery( 'SELECT (t1.name) FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY (((name)))' );
$this->assertEquals(
array( (object) array( '(t1.name)' => 'A' ) ),
$result
);

/*
* The following query fails with "ambiguous column name" in MySQL, but
* in SQLite, it works. It's OK to keep this difference as MySQL behaves
* rather strangely in this case:
*
* 1) This is OK in MySQL:
* SELECT t1.name AS col, 123 AS col ... GROUP BY col
* 2) This fails in MySQL:
* SELECT t1.name AS col, t2.name AS col ... GROUP BY col
*/
$this->assertQuery( 'SELECT t1.name AS col, t2.name AS col FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY col' );
}

public function testSelectGroupByAmbiguousColumnError(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT t1.name, t2.name FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY name' );
}

public function testSelectGroupByAmbiguousColumnErrorWithoutSelectList(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT 1 FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY name' );
}

public function testSelectHavingAmbiguousColumnResolution(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );
$this->assertQuery( 'INSERT INTO t1 (id, name) VALUES (1, "A"), (2, "A")' );
$this->assertQuery( 'INSERT INTO t2 (id, name) VALUES (1, "B1"), (2, "B2")' );

// The "name" column will be resolved to "t1.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t1.name FROM t1 JOIN t2 ON t2.id = t1.id HAVING name' );
$this->assertEquals( array(), $result );

// The "name" column will be resolved to "t2.name" as per the SELECT item.
$result = $this->assertQuery( 'SELECT t2.name FROM t1 JOIN t2 ON t2.id = t1.id HAVING name' );
$this->assertEquals( array(), $result );

// Parenthesized column reference can be used in both SELECT and GROUP BY lists.
$result = $this->assertQuery( 'SELECT (t1.name) FROM t1 JOIN t2 ON t2.id = t1.id HAVING (((name)))' );
$this->assertEquals( array(), $result );

/*
* The following query fails with "ambiguous column name" in MySQL, but
* in SQLite, it works. It's OK to keep this difference as MySQL behaves
* rather strangely in this case:
*
* 1) This is OK in MySQL:
* SELECT t1.name AS col, 123 AS col ... HAVING col
* 2) This fails in MySQL:
* SELECT t1.name AS col, t2.name AS col ... HAVING col
*/
$this->assertQuery( 'SELECT t1.name AS col, t2.name AS col FROM t1 JOIN t2 ON t2.id = t1.id HAVING col' );
}

public function testSelectHavingAmbiguousColumnError(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT t1.name, t2.name FROM t1 JOIN t2 ON t2.id = t1.id HAVING name' );
}

public function testSelectHavingAmbiguousColumnErrorWithoutSelectList(): void {
$this->assertQuery( 'CREATE TABLE t1 (id INT, name TEXT)' );
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );

$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'ambiguous column name: name' );
$this->assertQuery( 'SELECT 1 FROM t1 JOIN t2 ON t2.id = t1.id HAVING name' );
}

public function testRollbackNonExistentTransactionSavepoint(): void {
$this->expectException( 'WP_SQLite_Driver_Exception' );
$this->expectExceptionMessage( 'no such savepoint: sp1' );
Expand Down
Loading
Loading