Skip to content

Commit 417f649

Browse files
authored
test(linux): test the oldest and newest supported server releases (#3557)
1 parent 64a8e4e commit 417f649

30 files changed

Lines changed: 686 additions & 115 deletions

‎.github/workflows/build-linux.yml‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ jobs:
433433
manifest linux/build-aux/flatpak/app.tablepro.TablePro.Devel.json
434434
435435
integration:
436-
name: Docker tests (${{ matrix.package }})
436+
name: Docker tests (${{ matrix.package }}${{ matrix.release != 'default' && format(', {0}', matrix.release) || '' }})
437437
needs: [changes, fast]
438438
if: ${{ !cancelled() && needs.changes.outputs.run != 'false' && needs.fast.result == 'success' }}
439439
runs-on: ubuntu-24.04
@@ -448,11 +448,41 @@ jobs:
448448
- tablepro-driver-clickhouse
449449
- tablepro-ssh
450450
- tablepro-test-fixtures
451+
# Each fixture starts its engine's default release. An entry with
452+
# another release runs the driver's whole suite again on the
453+
# versions it names (docs/testing.md, Server releases).
454+
release: [default]
451455
include:
452456
- package: tablepro-ssh
453457
apt: openssh-client
454458
- package: tablepro-test-fixtures
455459
apt: openssh-client
460+
- package: tablepro-driver-postgres
461+
release: postgres-14
462+
postgres: "14"
463+
- package: tablepro-driver-postgres
464+
release: postgres-18
465+
postgres: "18"
466+
- package: tablepro-driver-mysql
467+
release: mysql-8.0-mariadb-10.11
468+
mysql: "8.0"
469+
mariadb: "10.11"
470+
- package: tablepro-driver-mysql
471+
release: mysql-9.7-mariadb-12.3
472+
mysql: "9.7"
473+
mariadb: "12.3"
474+
- package: tablepro-driver-mssql
475+
release: mssql-2017
476+
mssql: "2017"
477+
- package: tablepro-driver-mssql
478+
release: mssql-2019
479+
mssql: "2019"
480+
- package: tablepro-driver-mssql
481+
release: mssql-2025
482+
mssql: "2025"
483+
- package: tablepro-driver-clickhouse
484+
release: clickhouse-26.3
485+
clickhouse: "26.3"
456486
defaults:
457487
run:
458488
working-directory: linux
@@ -482,12 +512,17 @@ jobs:
482512
- name: Docker tests
483513
env:
484514
PACKAGE: ${{ matrix.package }}
515+
TABLEPRO_TEST_POSTGRES: ${{ matrix.postgres }}
516+
TABLEPRO_TEST_MYSQL: ${{ matrix.mysql }}
517+
TABLEPRO_TEST_MARIADB: ${{ matrix.mariadb }}
518+
TABLEPRO_TEST_MSSQL: ${{ matrix.mssql }}
519+
TABLEPRO_TEST_CLICKHOUSE: ${{ matrix.clickhouse }}
485520
run: cargo nextest run --locked --profile ci -p "$PACKAGE" --run-ignored only
486521
- name: Upload JUnit report
487522
if: always()
488523
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
489524
with:
490-
name: junit-${{ matrix.package }}
525+
name: junit-${{ matrix.package }}${{ matrix.release != 'default' && format('-{0}', matrix.release) || '' }}
491526
path: linux/target/nextest/ci/junit.xml
492527

493528
integration-session:

‎linux/README.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ Native Linux database client, the Linux sibling of the macOS TablePro app. The t
66

77
It connects to five engines: PostgreSQL, MySQL and MariaDB, SQL Server, ClickHouse and SQLite. Each connection session gets a window of its own. Nothing is released yet: no `linux-v*` tag exists. [ROADMAP.md](ROADMAP.md) lists what is built, what is not, and the release criteria.
88

9+
## Supported servers
10+
11+
TablePro supports the server releases their vendors still support. CI runs each driver's whole Docker suite on every release in the second column ([docs/testing.md](docs/testing.md#server-releases)).
12+
13+
| Engine | Tested releases | Not supported |
14+
|---|---|---|
15+
| PostgreSQL | 14, 17 and 18 | 13 and older, past end of life (13 on 2025-11-13). 19 until it is released |
16+
| MySQL | 8.0, 8.4 and 9.7 | 8.1 to 9.6, innovation releases past end of life. 5.7 and older |
17+
| MariaDB | 10.11, 11.4 and 12.3 | 10.6 and older, past end of life (10.6 on 2026-07-06). Rolling releases such as 13.0 |
18+
| SQL Server | 2017, 2019, 2022 and 2025 | 2016 and older, past extended support (2016 on 2026-07-14) |
19+
| ClickHouse | 26.3 and 26.8 | Monthly releases between long-term ones. 25.8 and older long-term releases, past end of life (25.8 on 2026-08-29) |
20+
| SQLite | the system library, 3.37.0 or newer | older libraries, which the driver refuses when it opens a file |
21+
22+
- PostgreSQL 14 reaches end of life on 2026-11-12.
23+
- MySQL 8.0 reached end of life on 2026-04-30, but Amazon RDS keeps 8.0 instances under paid Extended Support until 2029-07-31, so it stays tested.
24+
- MariaDB 11.8 and PostgreSQL 15 and 16 are supported between the tested releases, which bound them.
25+
926
## Install
1027

1128
These commands work from the first release, 0.1.0, on amd64 and arm64.

‎linux/crates/drivers/clickhouse/src/ddl.rs‎

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,32 @@ impl DdlDialect for ClickHouseDialect {
132132
let draft = &alteration.draft;
133133
let name = self.quote_identifier(&draft.name);
134134
let changed = alteration.changed;
135+
let mut default_written = false;
135136
if changed.column_type || changed.nullability {
136137
let resolved = draft.column_type.resolve(self, &draft.name)?;
137-
statements.push(alter(format!(
138-
"MODIFY COLUMN {name} {}",
139-
with_nullability(&resolved.sql, draft.nullable)
140-
)));
138+
let declaration = with_nullability(&resolved.sql, draft.nullable);
139+
// From 25.12 the server refuses to make a column non-nullable
140+
// without a DEFAULT to put in its NULLs. With none in the
141+
// draft, the type's own default fills them, as older servers
142+
// did, and is removed again.
143+
let fills_nulls = alteration.original.info.nullable && !draft.nullable;
144+
match default_sql(self, &draft.name, &draft.default, &resolved.column_type)? {
145+
Some(default) if fills_nulls => {
146+
statements.push(alter(format!("MODIFY COLUMN {name} {declaration} DEFAULT {default}")));
147+
default_written = true;
148+
}
149+
None if fills_nulls => {
150+
statements.push(alter(format!(
151+
"MODIFY COLUMN {name} {declaration} DEFAULT defaultValueOfTypeName({})",
152+
self.string_literal(&declaration)
153+
)));
154+
statements.push(alter(format!("MODIFY COLUMN {name} REMOVE DEFAULT")));
155+
default_written = true;
156+
}
157+
_ => statements.push(alter(format!("MODIFY COLUMN {name} {declaration}"))),
158+
}
141159
}
142-
if changed.default {
160+
if changed.default && !default_written {
143161
let column_type = draft.column_type.column_type(self, &draft.name)?;
144162
statements.push(alter(
145163
match default_sql(self, &draft.name, &draft.default, &column_type)? {
@@ -189,7 +207,11 @@ impl ClickHouseDialect {
189207
}
190208

191209
fn comment(&self, text: Option<&str>) -> String {
192-
let value = Value::Text(text.unwrap_or_default().to_owned());
210+
self.string_literal(text.unwrap_or_default())
211+
}
212+
213+
fn string_literal(&self, text: &str) -> String {
214+
let value = Value::Text(text.to_owned());
193215
SqlDialect::literal(self, &value, &ColumnType::untyped_text("String")).unwrap_or_else(|_| "''".to_owned())
194216
}
195217
}
@@ -417,12 +439,37 @@ mod tests {
417439
[
418440
"ALTER TABLE `analytics`.`events` RENAME COLUMN `note` TO `remark`",
419441
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `label` REMOVE DEFAULT",
420-
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` String",
442+
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` String DEFAULT defaultValueOfTypeName('String')",
443+
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` REMOVE DEFAULT",
421444
"ALTER TABLE `analytics`.`events` ADD COLUMN `tag` Nullable(String)",
422445
]
423446
);
424447
}
425448

449+
#[test]
450+
fn a_column_made_non_nullable_fills_its_nulls_with_the_drafts_default() {
451+
let plan = alter(&events(), |draft| {
452+
let note = &mut draft.columns[2];
453+
note.nullable = false;
454+
note.default = ColumnDefault::Literal(Value::Text("none".to_owned()));
455+
});
456+
457+
assert_eq!(
458+
sql(plan),
459+
["ALTER TABLE `analytics`.`events` MODIFY COLUMN `note` String DEFAULT 'none'"]
460+
);
461+
}
462+
463+
#[test]
464+
fn a_column_made_nullable_needs_no_default() {
465+
let plan = alter(&events(), |draft| draft.columns[1].nullable = true);
466+
467+
assert_eq!(
468+
sql(plan),
469+
["ALTER TABLE `analytics`.`events` MODIFY COLUMN `label` Nullable(String)"]
470+
);
471+
}
472+
426473
#[test]
427474
fn a_swap_steps_aside_through_a_temporary_name() {
428475
let plan = alter(&events(), |draft| {

‎linux/crates/drivers/clickhouse/tests/integration/tls.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,13 @@ async fn https_verify_full_ok() {
144144
.probe(within(CALL))
145145
.await
146146
.expect("the server answers over TLS");
147-
assert!(connection.server_info().version.starts_with("25."));
147+
let (major, minor) = fixture.version().reported();
148+
assert_eq!(
149+
(connection.server_info().major, connection.server_info().minor),
150+
(major, minor),
151+
"{}",
152+
connection.server_info().version
153+
);
148154
connection.close().await.expect("a close");
149155
}
150156

‎linux/crates/drivers/mssql/tests/integration/decode.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ async fn serverproperty_product_version_no_panic() {
2626

2727
let version = scalar(connection.as_ref(), "SELECT SERVERPROPERTY('ProductVersion')").await;
2828

29-
assert!(connection.server_info().major >= 16, "{:?}", connection.server_info());
29+
assert_eq!(
30+
connection.server_info().major,
31+
fixture.version().major(),
32+
"{:?}",
33+
connection.server_info()
34+
);
3035
assert!(text(&version).starts_with(&connection.server_info().major.to_string()));
3136
}
3237

‎linux/crates/drivers/mysql/tests/integration/conformance_engine.rs‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//! The connection conformance suite against MySQL 8.4 and MariaDB 11.4.
1+
//! The connection conformance suite against MySQL and MariaDB, on the
2+
//! releases `TABLEPRO_TEST_MYSQL` and `TABLEPRO_TEST_MARIADB` pick.
23
34
use std::marker::PhantomData;
45
use std::sync::Arc;
@@ -23,15 +24,15 @@ pub trait Server: Send + Sync + 'static {
2324
const FLAVOUR: MysqlFlavour;
2425
}
2526

26-
pub struct Mysql84;
27+
pub struct MysqlServer;
2728

28-
impl Server for Mysql84 {
29+
impl Server for MysqlServer {
2930
const FLAVOUR: MysqlFlavour = MysqlFlavour::Mysql;
3031
}
3132

32-
pub struct Mariadb114;
33+
pub struct MariadbServer;
3334

34-
impl Server for Mariadb114 {
35+
impl Server for MariadbServer {
3536
const FLAVOUR: MysqlFlavour = MysqlFlavour::Mariadb;
3637
}
3738

@@ -200,7 +201,7 @@ impl<S: Server> ConformanceDdlEngine for MySqlEngine<S> {
200201
/// MariaDB's `max_statement_time` stops a CREATE ... SELECT as it stops a
201202
/// SELECT. MySQL's `max_execution_time` bounds only read-only SELECTs, so
202203
/// it has no bounded DDL to lift and runs without this case.
203-
impl BoundedDdlEngine for MySqlEngine<Mariadb114> {
204+
impl BoundedDdlEngine for MySqlEngine<MariadbServer> {
204205
fn schema_change_exceeding_sql(&self, table: &TableRef, duration: Duration) -> String {
205206
format!(
206207
"CREATE TABLE {} AS SELECT SLEEP({}) AS id",
@@ -236,10 +237,10 @@ impl<S: Server> LargeTableEngine for MySqlEngine<S> {
236237
}
237238

238239
mod mysql_suite {
239-
use super::{MySqlEngine, Mysql84};
240+
use super::{MySqlEngine, MysqlServer};
240241

241242
tablepro_test_support::connection_conformance_suite! {
242-
engine: MySqlEngine<Mysql84>,
243+
engine: MySqlEngine<MysqlServer>,
243244
docker: true,
244245
endpoint: network,
245246
pool: pooled,
@@ -249,20 +250,20 @@ mod mysql_suite {
249250
}
250251

251252
tablepro_test_support::ddl_conformance_suite! {
252-
engine: MySqlEngine<Mysql84>,
253+
engine: MySqlEngine<MysqlServer>,
253254
docker: true,
254255
}
255256

256257
tablepro_test_support::large_table_conformance_suite! {
257-
engine: MySqlEngine<Mysql84>,
258+
engine: MySqlEngine<MysqlServer>,
258259
}
259260
}
260261

261262
mod mariadb_suite {
262-
use super::{Mariadb114, MySqlEngine};
263+
use super::{MariadbServer, MySqlEngine};
263264

264265
tablepro_test_support::connection_conformance_suite! {
265-
engine: MySqlEngine<Mariadb114>,
266+
engine: MySqlEngine<MariadbServer>,
266267
docker: true,
267268
endpoint: network,
268269
pool: pooled,
@@ -272,11 +273,11 @@ mod mariadb_suite {
272273
}
273274

274275
tablepro_test_support::ddl_conformance_suite! {
275-
engine: MySqlEngine<Mariadb114>,
276+
engine: MySqlEngine<MariadbServer>,
276277
docker: true,
277278
}
278279

279280
tablepro_test_support::large_table_conformance_suite! {
280-
engine: MySqlEngine<Mariadb114>,
281+
engine: MySqlEngine<MariadbServer>,
281282
}
282283
}

‎linux/crates/drivers/mysql/tests/integration/flavour_suite.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
//! What the driver promises on both MySQL 8.4 and MariaDB 11.4: the
2-
//! session it sets up, how it stops statements, and how saves fail.
1+
//! What the driver promises on both MySQL and MariaDB: the session it
2+
//! sets up, how it stops statements, and how saves fail.
33
44
use std::time::Duration;
55

@@ -23,11 +23,7 @@ async fn server_info_product_flavour() {
2323

2424
let info = connection.server_info();
2525
assert_eq!(info.product, product);
26-
let expected = match flavour {
27-
MysqlFlavour::Mysql => (8, 4),
28-
MysqlFlavour::Mariadb => (11, 4),
29-
};
30-
assert_eq!((info.major, info.minor), expected, "{}", info.version);
26+
assert_eq!((info.major, info.minor), fixture.server_version(), "{}", info.version);
3127
connection.close().await.expect("a close");
3228
}
3329
}

‎linux/crates/drivers/mysql/tests/integration/non_transactional.rs‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use tablepro_core::statement::GuardedStatement;
55
use tablepro_core::{DriverError, ForeignKeyMode, StatementOutcome, WriteBatch, WriteKind};
66
use tablepro_test_fixtures::MysqlFlavour;
77

8-
use crate::support::{CALL, FLAVOURS, connect, count, setup, start, statement, table, within};
8+
use crate::support::{CALL, FLAVOURS, connect, count, scalar, setup, start, statement, table, text, within};
99

1010
fn guarded(sql: &str, rows: u64) -> GuardedStatement {
1111
GuardedStatement::affecting_exactly(statement(sql), rows)
@@ -190,7 +190,9 @@ async fn aria_table_uses_statement_by_statement() {
190190
}
191191

192192
/// An InnoDB save rolls back, but what its trigger wrote into a MyISAM
193-
/// table stays, and the server's warning 1196 says so.
193+
/// table stays, and the server's warning 1196 says so. MySQL refuses
194+
/// the save with 1785 before anything lands where it enforces GTID
195+
/// consistency, as 9.7 does by default.
194196
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
195197
#[ignore = "requires docker"]
196198
async fn innodb_trigger_into_myisam_rollback_warning_1196_partially_applied() {
@@ -207,6 +209,9 @@ async fn innodb_trigger_into_myisam_rollback_warning_1196_partially_applied() {
207209
)
208210
.await;
209211

212+
let enforces_gtid_consistency = flavour == MysqlFlavour::Mysql
213+
&& text(&scalar(connection.as_ref(), "SELECT @@GLOBAL.enforce_gtid_consistency").await) == "ON";
214+
210215
let outcome = connection
211216
.write_batch(
212217
&batch(
@@ -220,19 +225,26 @@ async fn innodb_trigger_into_myisam_rollback_warning_1196_partially_applied() {
220225
)
221226
.await;
222227

223-
assert!(
224-
matches!(
225-
outcome,
226-
Err(DriverError::PartiallyApplied {
227-
applied: 1,
228-
statement_index: 1,
229-
..
230-
})
228+
let (expected, audited) = match enforces_gtid_consistency {
229+
true => (
230+
matches!(outcome, Err(DriverError::RolledBack { statement_index: 0, .. })),
231+
0,
231232
),
232-
"{flavour:?}: {outcome:?}"
233-
);
233+
false => (
234+
matches!(
235+
outcome,
236+
Err(DriverError::PartiallyApplied {
237+
applied: 1,
238+
statement_index: 1,
239+
..
240+
})
241+
),
242+
1,
243+
),
244+
};
245+
assert!(expected, "{flavour:?}: {outcome:?}");
234246
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM source").await, 0);
235-
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM audit").await, 1);
247+
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM audit").await, audited);
236248
connection.close().await.expect("a close");
237249
}
238250
}

0 commit comments

Comments
 (0)