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
39 changes: 37 additions & 2 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ jobs:
manifest linux/build-aux/flatpak/app.tablepro.TablePro.Devel.json

integration:
name: Docker tests (${{ matrix.package }})
name: Docker tests (${{ matrix.package }}${{ matrix.release != 'default' && format(', {0}', matrix.release) || '' }})
needs: [changes, fast]
if: ${{ !cancelled() && needs.changes.outputs.run != 'false' && needs.fast.result == 'success' }}
runs-on: ubuntu-24.04
Expand All @@ -447,11 +447,41 @@ jobs:
- tablepro-driver-clickhouse
- tablepro-ssh
- tablepro-test-fixtures
# Each fixture starts its engine's default release. An entry with
# another release runs the driver's whole suite again on the
# versions it names (docs/testing.md, Server releases).
release: [default]
include:
- package: tablepro-ssh
apt: openssh-client
- package: tablepro-test-fixtures
apt: openssh-client
- package: tablepro-driver-postgres
release: postgres-14
postgres: "14"
- package: tablepro-driver-postgres
release: postgres-18
postgres: "18"
- package: tablepro-driver-mysql
release: mysql-8.0-mariadb-10.11
mysql: "8.0"
mariadb: "10.11"
- package: tablepro-driver-mysql
release: mysql-9.7-mariadb-12.3
mysql: "9.7"
mariadb: "12.3"
- package: tablepro-driver-mssql
release: mssql-2017
mssql: "2017"
- package: tablepro-driver-mssql
release: mssql-2019
mssql: "2019"
- package: tablepro-driver-mssql
release: mssql-2025
mssql: "2025"
- package: tablepro-driver-clickhouse
release: clickhouse-26.3
clickhouse: "26.3"
defaults:
run:
working-directory: linux
Expand Down Expand Up @@ -481,12 +511,17 @@ jobs:
- name: Docker tests
env:
PACKAGE: ${{ matrix.package }}
TABLEPRO_TEST_POSTGRES: ${{ matrix.postgres }}
TABLEPRO_TEST_MYSQL: ${{ matrix.mysql }}
TABLEPRO_TEST_MARIADB: ${{ matrix.mariadb }}
TABLEPRO_TEST_MSSQL: ${{ matrix.mssql }}
TABLEPRO_TEST_CLICKHOUSE: ${{ matrix.clickhouse }}
run: cargo nextest run --locked --profile ci -p "$PACKAGE" --run-ignored only
- name: Upload JUnit report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-${{ matrix.package }}
name: junit-${{ matrix.package }}${{ matrix.release != 'default' && format('-{0}', matrix.release) || '' }}
path: linux/target/nextest/ci/junit.xml

integration-session:
Expand Down
17 changes: 17 additions & 0 deletions linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ Native Linux database client, the Linux sibling of the macOS TablePro app. The t

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.

## Supported servers

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)).

| Engine | Tested releases | Not supported |
|---|---|---|
| PostgreSQL | 14, 17 and 18 | 13 and older, past end of life (13 on 2025-11-13). 19 until it is released |
| MySQL | 8.0, 8.4 and 9.7 | 8.1 to 9.6, innovation releases past end of life. 5.7 and older |
| 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 |
| SQL Server | 2017, 2019, 2022 and 2025 | 2016 and older, past extended support (2016 on 2026-07-14) |
| 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) |
| SQLite | the system library, 3.37.0 or newer | older libraries, which the driver refuses when it opens a file |

- PostgreSQL 14 reaches end of life on 2026-11-12.
- 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.
- MariaDB 11.8 and PostgreSQL 15 and 16 are supported between the tested releases, which bound them.

## Install

These commands work from the first release, 0.1.0, on amd64 and arm64.
Expand Down
61 changes: 54 additions & 7 deletions linux/crates/drivers/clickhouse/src/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,32 @@ impl DdlDialect for ClickHouseDialect {
let draft = &alteration.draft;
let name = self.quote_identifier(&draft.name);
let changed = alteration.changed;
let mut default_written = false;
if changed.column_type || changed.nullability {
let resolved = draft.column_type.resolve(self, &draft.name)?;
statements.push(alter(format!(
"MODIFY COLUMN {name} {}",
with_nullability(&resolved.sql, draft.nullable)
)));
let declaration = with_nullability(&resolved.sql, draft.nullable);
// From 25.12 the server refuses to make a column non-nullable
// without a DEFAULT to put in its NULLs. With none in the
// draft, the type's own default fills them, as older servers
// did, and is removed again.
let fills_nulls = alteration.original.info.nullable && !draft.nullable;
match default_sql(self, &draft.name, &draft.default, &resolved.column_type)? {
Some(default) if fills_nulls => {
statements.push(alter(format!("MODIFY COLUMN {name} {declaration} DEFAULT {default}")));
default_written = true;
}
None if fills_nulls => {
statements.push(alter(format!(
"MODIFY COLUMN {name} {declaration} DEFAULT defaultValueOfTypeName({})",
self.string_literal(&declaration)
)));
statements.push(alter(format!("MODIFY COLUMN {name} REMOVE DEFAULT")));
default_written = true;
}
_ => statements.push(alter(format!("MODIFY COLUMN {name} {declaration}"))),
}
}
if changed.default {
if changed.default && !default_written {
let column_type = draft.column_type.column_type(self, &draft.name)?;
statements.push(alter(
match default_sql(self, &draft.name, &draft.default, &column_type)? {
Expand Down Expand Up @@ -189,7 +207,11 @@ impl ClickHouseDialect {
}

fn comment(&self, text: Option<&str>) -> String {
let value = Value::Text(text.unwrap_or_default().to_owned());
self.string_literal(text.unwrap_or_default())
}

fn string_literal(&self, text: &str) -> String {
let value = Value::Text(text.to_owned());
SqlDialect::literal(self, &value, &ColumnType::untyped_text("String")).unwrap_or_else(|_| "''".to_owned())
}
}
Expand Down Expand Up @@ -417,12 +439,37 @@ mod tests {
[
"ALTER TABLE `analytics`.`events` RENAME COLUMN `note` TO `remark`",
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `label` REMOVE DEFAULT",
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` String",
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` String DEFAULT defaultValueOfTypeName('String')",
"ALTER TABLE `analytics`.`events` MODIFY COLUMN `remark` REMOVE DEFAULT",
"ALTER TABLE `analytics`.`events` ADD COLUMN `tag` Nullable(String)",
]
);
}

#[test]
fn a_column_made_non_nullable_fills_its_nulls_with_the_drafts_default() {
let plan = alter(&events(), |draft| {
let note = &mut draft.columns[2];
note.nullable = false;
note.default = ColumnDefault::Literal(Value::Text("none".to_owned()));
});

assert_eq!(
sql(plan),
["ALTER TABLE `analytics`.`events` MODIFY COLUMN `note` String DEFAULT 'none'"]
);
}

#[test]
fn a_column_made_nullable_needs_no_default() {
let plan = alter(&events(), |draft| draft.columns[1].nullable = true);

assert_eq!(
sql(plan),
["ALTER TABLE `analytics`.`events` MODIFY COLUMN `label` Nullable(String)"]
);
}

#[test]
fn a_swap_steps_aside_through_a_temporary_name() {
let plan = alter(&events(), |draft| {
Expand Down
8 changes: 7 additions & 1 deletion linux/crates/drivers/clickhouse/tests/integration/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@ async fn https_verify_full_ok() {
.probe(within(CALL))
.await
.expect("the server answers over TLS");
assert!(connection.server_info().version.starts_with("25."));
let (major, minor) = fixture.version().reported();
assert_eq!(
(connection.server_info().major, connection.server_info().minor),
(major, minor),
"{}",
connection.server_info().version
);
connection.close().await.expect("a close");
}

Expand Down
7 changes: 6 additions & 1 deletion linux/crates/drivers/mssql/tests/integration/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ async fn serverproperty_product_version_no_panic() {

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

assert!(connection.server_info().major >= 16, "{:?}", connection.server_info());
assert_eq!(
connection.server_info().major,
fixture.version().major(),
"{:?}",
connection.server_info()
);
assert!(text(&version).starts_with(&connection.server_info().major.to_string()));
}

Expand Down
29 changes: 15 additions & 14 deletions linux/crates/drivers/mysql/tests/integration/conformance_engine.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! The connection conformance suite against MySQL 8.4 and MariaDB 11.4.
//! The connection conformance suite against MySQL and MariaDB, on the
//! releases `TABLEPRO_TEST_MYSQL` and `TABLEPRO_TEST_MARIADB` pick.

use std::marker::PhantomData;
use std::sync::Arc;
Expand All @@ -23,15 +24,15 @@ pub trait Server: Send + Sync + 'static {
const FLAVOUR: MysqlFlavour;
}

pub struct Mysql84;
pub struct MysqlServer;

impl Server for Mysql84 {
impl Server for MysqlServer {
const FLAVOUR: MysqlFlavour = MysqlFlavour::Mysql;
}

pub struct Mariadb114;
pub struct MariadbServer;

impl Server for Mariadb114 {
impl Server for MariadbServer {
const FLAVOUR: MysqlFlavour = MysqlFlavour::Mariadb;
}

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

mod mysql_suite {
use super::{MySqlEngine, Mysql84};
use super::{MySqlEngine, MysqlServer};

tablepro_test_support::connection_conformance_suite! {
engine: MySqlEngine<Mysql84>,
engine: MySqlEngine<MysqlServer>,
docker: true,
endpoint: network,
pool: pooled,
Expand All @@ -249,20 +250,20 @@ mod mysql_suite {
}

tablepro_test_support::ddl_conformance_suite! {
engine: MySqlEngine<Mysql84>,
engine: MySqlEngine<MysqlServer>,
docker: true,
}

tablepro_test_support::large_table_conformance_suite! {
engine: MySqlEngine<Mysql84>,
engine: MySqlEngine<MysqlServer>,
}
}

mod mariadb_suite {
use super::{Mariadb114, MySqlEngine};
use super::{MariadbServer, MySqlEngine};

tablepro_test_support::connection_conformance_suite! {
engine: MySqlEngine<Mariadb114>,
engine: MySqlEngine<MariadbServer>,
docker: true,
endpoint: network,
pool: pooled,
Expand All @@ -272,11 +273,11 @@ mod mariadb_suite {
}

tablepro_test_support::ddl_conformance_suite! {
engine: MySqlEngine<Mariadb114>,
engine: MySqlEngine<MariadbServer>,
docker: true,
}

tablepro_test_support::large_table_conformance_suite! {
engine: MySqlEngine<Mariadb114>,
engine: MySqlEngine<MariadbServer>,
}
}
10 changes: 3 additions & 7 deletions linux/crates/drivers/mysql/tests/integration/flavour_suite.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! What the driver promises on both MySQL 8.4 and MariaDB 11.4: the
//! session it sets up, how it stops statements, and how saves fail.
//! What the driver promises on both MySQL and MariaDB: the session it
//! sets up, how it stops statements, and how saves fail.

use std::time::Duration;

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

let info = connection.server_info();
assert_eq!(info.product, product);
let expected = match flavour {
MysqlFlavour::Mysql => (8, 4),
MysqlFlavour::Mariadb => (11, 4),
};
assert_eq!((info.major, info.minor), expected, "{}", info.version);
assert_eq!((info.major, info.minor), fixture.server_version(), "{}", info.version);
connection.close().await.expect("a close");
}
}
Expand Down
38 changes: 25 additions & 13 deletions linux/crates/drivers/mysql/tests/integration/non_transactional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tablepro_core::statement::GuardedStatement;
use tablepro_core::{DriverError, ForeignKeyMode, StatementOutcome, WriteBatch, WriteKind};
use tablepro_test_fixtures::MysqlFlavour;

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

fn guarded(sql: &str, rows: u64) -> GuardedStatement {
GuardedStatement::affecting_exactly(statement(sql), rows)
Expand Down Expand Up @@ -190,7 +190,9 @@ async fn aria_table_uses_statement_by_statement() {
}

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

let enforces_gtid_consistency = flavour == MysqlFlavour::Mysql
&& text(&scalar(connection.as_ref(), "SELECT @@GLOBAL.enforce_gtid_consistency").await) == "ON";

let outcome = connection
.write_batch(
&batch(
Expand All @@ -220,19 +225,26 @@ async fn innodb_trigger_into_myisam_rollback_warning_1196_partially_applied() {
)
.await;

assert!(
matches!(
outcome,
Err(DriverError::PartiallyApplied {
applied: 1,
statement_index: 1,
..
})
let (expected, audited) = match enforces_gtid_consistency {
true => (
matches!(outcome, Err(DriverError::RolledBack { statement_index: 0, .. })),
0,
),
"{flavour:?}: {outcome:?}"
);
false => (
matches!(
outcome,
Err(DriverError::PartiallyApplied {
applied: 1,
statement_index: 1,
..
})
),
1,
),
};
assert!(expected, "{flavour:?}: {outcome:?}");
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM source").await, 0);
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM audit").await, 1);
assert_eq!(count(connection.as_ref(), "SELECT COUNT(*) FROM audit").await, audited);
connection.close().await.expect("a close");
}
}
Loading
Loading