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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- SSH tunnels using the None auth method now work on iPhone and iPad, not just the Mac. A connection synced from the Mac with None auth no longer fails to connect. (#1912)
- Browsing a Trino table no longer fails with a syntax error. Trino requires `OFFSET` before `LIMIT`, so paging now uses `OFFSET` and `FETCH FIRST`. (#1936)

## [0.59.0] - 2026-07-21

Expand Down
3 changes: 2 additions & 1 deletion Plugins/TrinoDriverPlugin/TrinoPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ final class TrinoPlugin: NSObject, TableProPlugin, DriverPlugin {
regexSyntax: .regexpLike,
booleanLiteralStyle: .truefalse,
likeEscapeStyle: .explicit,
paginationStyle: .limit
paginationStyle: .offsetFetch,
offsetFetchOrderBy: ""
)

static let explainVariants: [ExplainVariant] = [
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Core/Database/FilterSQLGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ extension FilterSQLGenerator {

if dialect.paginationStyle == .offsetFetch {
let orderBy = dialect.offsetFetchOrderBy
sql += "\n\(orderBy) OFFSET 0 ROWS FETCH NEXT \(limit) ROWS ONLY"
let orderByPrefix = orderBy.isEmpty ? "" : "\(orderBy) "
sql += "\n\(orderByPrefix)OFFSET 0 ROWS FETCH NEXT \(limit) ROWS ONLY"
} else {
sql += "\nLIMIT \(limit)"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ extension PluginMetadataRegistry {
regexSyntax: .regexpLike,
booleanLiteralStyle: .truefalse,
likeEscapeStyle: .explicit,
paginationStyle: .limit
paginationStyle: .offsetFetch,
offsetFetchOrderBy: ""
),
statementCompletions: [],
columnTypesByCategory: [
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Core/Services/Query/TableQueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ struct TableQueryBuilder {
return orderBy
}
guard dialect?.paginationStyle == .offsetFetch else { return nil }
return dialect?.offsetFetchOrderBy ?? "ORDER BY (SELECT NULL)"
let defaultOrderBy = dialect?.offsetFetchOrderBy ?? "ORDER BY (SELECT NULL)"
return defaultOrderBy.isEmpty ? nil : defaultOrderBy
}

private func buildOrderByClause(sortState: SortState?, columns: [String]) -> String? {
Expand Down
26 changes: 26 additions & 0 deletions TableProTests/Core/Database/FilterSQLGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ struct FilterSQLGeneratorTests {
offsetFetchOrderBy: "ORDER BY 1"
)

private static let trinoDialect = SQLDialectDescriptor(
identifierQuote: "\"", keywords: [], functions: [], dataTypes: [],
regexSyntax: .regexpLike, booleanLiteralStyle: .truefalse,
likeEscapeStyle: .explicit, paginationStyle: .offsetFetch,
offsetFetchOrderBy: ""
)

// MARK: - Per-Operator Tests (MySQL)

@Test("Equal operator generates correct condition")
Expand Down Expand Up @@ -826,6 +833,25 @@ struct FilterSQLGeneratorTests {
#expect(result.contains("SELECT * FROM `users`"))
}

@Test("Trino paging emits OFFSET before FETCH FIRST with no synthetic ORDER BY")
func testPreviewSQLTrinoOffsetFetch() {
let generator = FilterSQLGenerator(dialect: Self.trinoDialect)
let result = generator.generatePreviewSQL(
tableName: "pseudo_columns", schemaName: "jdbc", filters: [], limit: 1_000
)
#expect(result.contains("OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY"))
#expect(!result.contains("LIMIT"))
#expect(!result.contains("ORDER BY"))
}

@Test("Oracle paging keeps its default ORDER BY before OFFSET and FETCH FIRST")
func testPreviewSQLOracleOffsetFetch() {
let generator = FilterSQLGenerator(dialect: Self.oracleDialect)
let result = generator.generatePreviewSQL(tableName: "users", filters: [], limit: 1_000)
#expect(result.contains("ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY"))
#expect(!result.contains("LIMIT"))
}

// MARK: - Edge Cases

@Test("Between with missing secondValue returns nil")
Expand Down
69 changes: 69 additions & 0 deletions TableProTests/Core/Services/TableQueryBuilderFilterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,75 @@ struct TableQueryBuilderFilteredCountTests {
}
}

@Suite("Table Query Builder - Pagination Clause")
struct TableQueryBuilderPaginationTests {
private static let trinoDialect = SQLDialectDescriptor(
identifierQuote: "\"", keywords: [], functions: [], dataTypes: [],
regexSyntax: .regexpLike, booleanLiteralStyle: .truefalse,
likeEscapeStyle: .explicit, paginationStyle: .offsetFetch, offsetFetchOrderBy: ""
)

private static let mssqlDialect = SQLDialectDescriptor(
identifierQuote: "[", keywords: [], functions: [], dataTypes: [],
regexSyntax: .unsupported, booleanLiteralStyle: .numeric,
likeEscapeStyle: .explicit, paginationStyle: .offsetFetch
)

private static let mysqlDialect = SQLDialectDescriptor(
identifierQuote: "`", keywords: [], functions: [], dataTypes: [],
regexSyntax: .regexp, booleanLiteralStyle: .numeric,
likeEscapeStyle: .implicit, paginationStyle: .limit
)

private func builder(_ dialect: SQLDialectDescriptor) -> TableQueryBuilder {
TableQueryBuilder(databaseType: .postgresql, dialect: dialect)
}

private func enabledFilter(_ column: String, _ value: String) -> TableFilter {
var filter = TableFilter()
filter.columnName = column
filter.filterOperator = .equal
filter.value = value
filter.isEnabled = true
return filter
}

@Test("Trino base query pages with OFFSET before FETCH FIRST and no ORDER BY")
func trinoBaseQueryOffsetFetch() {
let query = builder(Self.trinoDialect).buildBaseQuery(
tableName: "pseudo_columns", schemaName: "jdbc", limit: 1_000, offset: 0
)
#expect(query.contains("\"jdbc\".\"pseudo_columns\""))
#expect(query.contains("OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY"))
#expect(!query.contains("LIMIT"))
#expect(!query.contains("ORDER BY"))
}

@Test("Trino filtered query keeps the WHERE and pages with OFFSET/FETCH FIRST")
func trinoFilteredQueryOffsetFetch() {
let query = builder(Self.trinoDialect).buildFilteredQuery(
tableName: "orders", filters: [enabledFilter("status", "open")], limit: 500, offset: 500
)
#expect(query.contains("WHERE"))
#expect(query.contains("OFFSET 500 ROWS FETCH NEXT 500 ROWS ONLY"))
#expect(!query.contains("LIMIT"))
}

@Test("MSSQL base query injects its default ORDER BY before OFFSET/FETCH FIRST")
func mssqlBaseQueryKeepsDefaultOrderBy() {
let query = builder(Self.mssqlDialect).buildBaseQuery(tableName: "users", limit: 100, offset: 0)
#expect(query.contains("ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"))
#expect(!query.contains("LIMIT"))
}

@Test("LIMIT-style dialect pages with LIMIT then OFFSET")
func limitStyleBaseQuery() {
let query = builder(Self.mysqlDialect).buildBaseQuery(tableName: "users", limit: 200, offset: 0)
#expect(query.contains("LIMIT 200 OFFSET 0"))
#expect(!query.contains("FETCH NEXT"))
}
}

@Suite("Table Query Builder - NoSQL Nil Dialect Fallback")
struct TableQueryBuilderNoSQLTests {
// MongoDB has no SQL dialect — should produce bare SELECT without WHERE
Expand Down
2 changes: 1 addition & 1 deletion docs/databases/trino.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Trino requires TLS for any authentication. If you pick Password or JWT, set an S

A Trino connection is scoped to a catalog and schema, but every query can name objects in full as `catalog.schema.table`. TablePro shows catalogs from `SHOW CATALOGS`, schemas from `SHOW SCHEMAS`, and tables and columns from each catalog's `information_schema`. Materialized views appear alongside tables and views. Expand a catalog to see its schemas, then a schema to see its tables. Table row counts come from `SHOW STATS`, and table and column comments come from `information_schema` and the `system.metadata` tables.

Identifiers are quoted with double quotes. Results page with standard `LIMIT` and `OFFSET`.
Identifiers are quoted with double quotes. Results page with `OFFSET` and `FETCH FIRST`, the order Trino's grammar requires.

## Types

Expand Down
Loading