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
2 changes: 1 addition & 1 deletion .github/hub-ratchet-baseline.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"MenuValidationContext stored properties": 86,
"menu validation `case #selector` arms": 115,
"ShortcutAction cases": 98,
"ShortcutAction cases": 103,
"TabType cases": 9,
"SettingsPane cases": 12,
"ActiveSheet cases": 17,
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Generate UUID and Generate UUIDv7 in Set Value, the row inspector and Fill Column, a new value for each cell. (#3443)
- Edit > Set Value in the menu bar, with rows in Settings > Keyboard. (#3443)
- New UUID in a UUID primary key the server does not fill, on Add Row, Duplicate Row and Paste. (#3443)

### Changed

- Set Value applies to every selected cell, or to the clicked column across the selected rows. (#3443)
- SQL functions in Set Value and the row inspector listed per database and only where they fit the column. (#3443)
- UUID functions in SQL Functions replaced by Generate UUID, which writes the value itself. (#3443)
- Text typed into a column that is not a date or time saved as typed, even when it reads like `now()`. (#3443)
- Set NULL and Set DEFAULT in the row inspector shown only where the column takes them, as in the data grid. (#3443)
- Default only on new rows on SQLite, libSQL, Turso, D1, ClickHouse, Databend, Snowflake, Trino, Cassandra. (#3443)
- Pending SQL functions in the data grid drawn like a pending default. (#3443)
- PluginKit ABI 39, additive: existing plugins keep loading. (#3443)

### Fixed

- Every ClickHouse query after connecting failing with `UNKNOWN_SETTING` on 23.8.0 to 23.8.8. (#3444)
Expand All @@ -17,6 +34,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ClickHouse SQL export writing the current database's table structure for a table in another database.
- Opening a ClickHouse executable function's definition showing a syntax error.
- SQL Server 2016 RTM offered a `CREATE OR ALTER VIEW` template it cannot run, and Azure SQL a drop-and-create one.
- `gen_random_uuid()`, `UUID()`, `NEWID()` and `datetime('now')` from SQL Functions saved as quoted text. (#3443)
- SQL functions saved as text on SQL Server, Teradata, Spanner and BigQuery. (#3443)
- `NOW()` offered on SQLite, Oracle, Cassandra and MongoDB, where it fails or saves the text. (#3443)
- SQLite's `datetime('now')` offered on DuckDB and `gen_random_uuid()` on Redshift. (#3443)
- Stored text such as `CURRENT_DATE` turning into today's date on Duplicate Row or Restore Previous Values. (#3443)
- Copy as INSERT and Copy as UPDATE quoting a staged SQL function. (#3443)
- Default on a saved row failing on SQLite, and storing `__DEFAULT__` on Teradata and DynamoDB. (#3443)
- Duplicate Row and Paste failing to save on a UUID primary key with no default. (#3443)
- Date functions and a date picker offered on Cassandra and ScyllaDB `timeuuid` columns. (#3443)
- MariaDB `uuid` columns read as `CHAR`. (#3443)
- Empty field in the multi-row inspector after the selected rows were given different values. (#3443)

## [0.81.0] - 2026-10-16

Expand Down
5 changes: 5 additions & 0 deletions Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ let package = Package(
dependencies: ["TableProBigtableCore", "TableProGoogleCloud"],
path: "Tests/TableProBigtableCoreTests"
),
.testTarget(
name: "TableProSpannerCoreTests",
dependencies: ["TableProSpannerCore"],
path: "Tests/TableProSpannerCoreTests"
),
.testTarget(
name: "TableProTabularIOTests",
dependencies: ["TableProTabularIO", "TableProPluginKit"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ public struct SpannerRowChange: Sendable, Equatable {
public let kind: Kind
public let cellChanges: [CellChange]
public let originalRow: [SpannerCell]?

public init(rowIndex: Int, kind: Kind, cellChanges: [CellChange] = [], originalRow: [SpannerCell]? = nil) {
// Keyed by name, as the writer collapses a row's cell changes to one value per column.
public let expressionColumns: Set<String>

public init(
rowIndex: Int,
kind: Kind,
cellChanges: [CellChange] = [],
originalRow: [SpannerCell]? = nil,
expressionColumns: Set<String> = []
) {
self.rowIndex = rowIndex
self.kind = kind
self.cellChanges = cellChanges
self.originalRow = originalRow
self.expressionColumns = expressionColumns
}
}

Expand Down Expand Up @@ -108,7 +117,9 @@ internal struct SpannerRowEditTarget {
?? Self.lastValuePerColumn(change.cellChanges)
let written = cells.filter { !Self.isDefaultSentinel($0.value) }
guard written.isEmpty else {
return Self.insertStatement(table: table, cells: written, dialect: dialect)
return Self.insertStatement(
table: table, cells: written, dialect: dialect, expressionColumns: change.expressionColumns
)
}
guard let firstColumn = columns.first else { return nil }
return SpannerRenderedStatement(
Expand All @@ -122,6 +133,7 @@ internal struct SpannerRowEditTarget {
let assignments = Self.lastValuePerColumn(change.cellChanges).map { cell in
let column = dialect.quoteIdentifier(cell.column)
guard !Self.isDefaultSentinel(cell.value) else { return "\(column) = DEFAULT" }
if let sql = Self.expression(cell, in: change.expressionColumns) { return "\(column) = \(sql)" }
parameters.append(cell.value)
return "\(column) = ?"
}
Expand All @@ -139,16 +151,28 @@ internal struct SpannerRowEditTarget {
static func insertStatement(
table: String,
cells: [SpannerRowChange.CellChange],
dialect: SpannerDialect
dialect: SpannerDialect,
expressionColumns: Set<String> = []
) -> SpannerRenderedStatement {
let names = cells.map { dialect.quoteIdentifier($0.column) }.joined(separator: ", ")
let placeholders = Array(repeating: "?", count: cells.count).joined(separator: ", ")
var parameters: [SpannerCell] = []
let values = cells.map { cell -> String in
if let sql = expression(cell, in: expressionColumns) { return sql }
parameters.append(cell.value)
return "?"
}
return SpannerRenderedStatement(
sql: "INSERT INTO \(table) (\(names)) VALUES (\(placeholders))",
parameters: cells.map(\.value)
sql: "INSERT INTO \(table) (\(names)) VALUES (\(values.joined(separator: ", ")))",
parameters: parameters
)
}

// The host checked a marked cell's text against the engine's vocabulary before handing it over.
private static func expression(_ cell: SpannerRowChange.CellChange, in expressionColumns: Set<String>) -> String? {
guard expressionColumns.contains(cell.column), case .text(let sql) = cell.value else { return nil }
return sql
}

private func keyPredicate(for change: SpannerRowChange) -> (sql: String, parameters: [SpannerCell])? {
guard !primaryKey.isEmpty, let originalRow = change.originalRow else { return nil }
var terms: [String] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import TableProSpannerCore
import Testing

struct SpannerRowEditSQLExpressionTests {
private let columns = ["id", "created_at", "name"]
private let originalRow: [SpannerCell] = [.text("1"), .null, .text("a")]

private func statements(
_ change: SpannerRowChange,
insertedRows: [Int: [SpannerCell]] = [:]
) -> [SpannerRenderedStatement]? {
SpannerRowEditSQL.statements(
schema: "",
table: "t",
columns: columns,
primaryKey: ["id"],
changes: [change],
insertedRows: insertedRows,
deletedRows: [],
insertedRowIndices: Set(insertedRows.keys),
dialect: .postgreSQL
)
}

@Test("A marked insert cell is written as SQL and the other cells stay bound")
func markedInsertCell() {
let change = SpannerRowChange(rowIndex: 0, kind: .insert, expressionColumns: ["created_at"])
let written = statements(change, insertedRows: [0: [.text("1"), .text("CURRENT_TIMESTAMP"), .text("a")]])
#expect(written == [SpannerRenderedStatement(
sql: #"INSERT INTO "t" ("id", "created_at", "name") VALUES (?, CURRENT_TIMESTAMP, ?)"#,
parameters: [.text("1"), .text("a")]
)])
}

@Test("A marked update cell is written as SQL")
func markedUpdateCell() {
let change = SpannerRowChange(
rowIndex: 0,
kind: .update,
cellChanges: [SpannerRowChange.CellChange(column: "created_at", value: .text("CURRENT_TIMESTAMP"))],
originalRow: originalRow,
expressionColumns: ["created_at"]
)
#expect(statements(change) == [SpannerRenderedStatement(
sql: #"UPDATE "t" SET "created_at" = CURRENT_TIMESTAMP WHERE "id" = ?"#,
parameters: [.text("1")]
)])
}

@Test("Text that spells a function is bound when its cell is not marked")
func unmarkedTextIsBound() {
let change = SpannerRowChange(
rowIndex: 0,
kind: .update,
cellChanges: [SpannerRowChange.CellChange(column: "name", value: .text("CURRENT_TIMESTAMP"))],
originalRow: originalRow
)
#expect(statements(change) == [SpannerRenderedStatement(
sql: #"UPDATE "t" SET "name" = ? WHERE "id" = ?"#,
parameters: [.text("CURRENT_TIMESTAMP"), .text("1")]
)])
}
}
2 changes: 1 addition & 1 deletion Plugins/BeancountDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>Beancount</string>
Expand Down
2 changes: 2 additions & 0 deletions Plugins/BigQueryDriverPlugin/BigQueryPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ internal final class BigQueryPluginDriver: PluginDatabaseDriver, @unchecked Send
.cancelQuery,
.materializedViews,
.dataCompare,
.cellExpressions,
.uuidCellText,
]
}

Expand Down
28 changes: 24 additions & 4 deletions Plugins/BigQueryDriverPlugin/BigQueryStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,28 @@ internal struct BigQueryStatementGenerator {
) -> Statement? {
guard let firstColumn = columns.first else { return nil }
let values = insertValues(for: change, insertedRowData: insertedRowData)
let expressionColumns = change.expressionColumns ?? []
var names: [String] = []
var placeholders: [String] = []
var parameters: [PluginCellValue] = []
for column in columns {
for (index, column) in columns.enumerated() {
guard let value = values[column], !Self.isDefaultSentinel(value) else { continue }
names.append(GoogleSQLLiteral.quotedIdentifier(column))
parameters.append(value)
if let sql = Self.expression(value, isMarked: expressionColumns.contains(index)) {
placeholders.append(sql)
} else {
placeholders.append(Self.placeholder)
parameters.append(value)
}
}

guard !names.isEmpty else {
let target = GoogleSQLLiteral.quotedIdentifier(firstColumn)
return (statement: "INSERT INTO \(qualifiedTable) (\(target)) VALUES (DEFAULT)", parameters: [])
}

let placeholders = Array(repeating: Self.placeholder, count: parameters.count).joined(separator: ", ")
let statement = "INSERT INTO \(qualifiedTable) (\(names.joined(separator: ", "))) VALUES (\(placeholders))"
let statement = "INSERT INTO \(qualifiedTable) (\(names.joined(separator: ", "))) "
+ "VALUES (\(placeholders.joined(separator: ", ")))"
return (statement: statement, parameters: parameters)
}

Expand All @@ -113,10 +120,17 @@ internal struct BigQueryStatementGenerator {

private func updateStatement(for change: PluginRowChange) -> Statement? {
guard let key = keyCondition(for: change) else { return nil }
let expressionColumns = change.expressionColumns ?? []
var parameters: [PluginCellValue] = []
let assignments = change.cellChanges.map { cellChange -> String in
let column = GoogleSQLLiteral.quotedIdentifier(cellChange.columnName)
guard !Self.isDefaultSentinel(cellChange.newValue) else { return "\(column) = DEFAULT" }
if let sql = Self.expression(
cellChange.newValue,
isMarked: expressionColumns.contains(cellChange.columnIndex)
) {
return "\(column) = \(sql)"
}
parameters.append(cellChange.newValue)
return "\(column) = \(Self.placeholder)"
}
Expand Down Expand Up @@ -174,4 +188,10 @@ internal struct BigQueryStatementGenerator {
private static func isDefaultSentinel(_ value: PluginCellValue) -> Bool {
value.asText == defaultSentinel
}

// The host checked a marked cell's text against the engine's vocabulary before handing it over.
private static func expression(_ value: PluginCellValue, isMarked: Bool) -> String? {
guard isMarked, case .text(let sql) = value else { return nil }
return sql
}
}
2 changes: 1 addition & 1 deletion Plugins/BigQueryDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/BigtableDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<key>TableProMinAppVersion</key>
<string>0.81.0</string>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>Bigtable</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/CSVExportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
<key>TableProProvidesExportFormatIds</key>
<array>
<string>csv</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/CSVImportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
<key>TableProProvidesImportFormatIds</key>
<array>
<string>csv</string>
Expand Down
2 changes: 2 additions & 0 deletions Plugins/CassandraDriverPlugin/CassandraPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen
.parameterizedQueries,
.materializedViews,
.alterTableDDL,
.cellExpressions,
.uuidCellText,
]
}

Expand Down
19 changes: 8 additions & 11 deletions Plugins/CassandraDriverPlugin/CassandraRowWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ enum CassandraRowWriter {
static let defaultSentinel = "__DEFAULT__"
static let unappliedMarker = "[applied]"

private static let functionCalls: Set<String> = [
"now()", "uuid()", "currenttimestamp()", "currentdate()", "currenttime()", "currenttimeuuid()",
"totimestamp(now())", "todate(now())"
]

static func rowWrites(
keyspace: String?,
table: String,
Expand Down Expand Up @@ -82,13 +77,14 @@ enum CassandraRowWriter {
))
}
let row = values ?? rowFromCellChanges(change)
let expressionColumns = change.expressionColumns ?? []
var names: [String] = []
var placeholders: [String] = []
var parameters: [PluginCellValue] = []
for (index, value) in row.enumerated() where index < columns.count {
if value.asText == defaultSentinel { continue }
names.append(CassandraBrowseRenderer.quote(columns[index]))
if let call = functionCall(value) {
if let call = expression(value, isMarked: expressionColumns.contains(index)) {
placeholders.append(call)
} else {
placeholders.append("?")
Expand All @@ -111,6 +107,7 @@ enum CassandraRowWriter {

func update(_ change: PluginRowChange) throws -> PluginRowWrite? {
guard !change.cellChanges.isEmpty else { return nil }
let expressionColumns = change.expressionColumns ?? []
var assignments: [String] = []
var parameters: [PluginCellValue] = []
for cell in change.cellChanges {
Expand All @@ -128,7 +125,7 @@ enum CassandraRowWriter {
))
}
let column = CassandraBrowseRenderer.quote(cell.columnName)
if let call = functionCall(cell.newValue) {
if let call = expression(cell.newValue, isMarked: expressionColumns.contains(cell.columnIndex)) {
assignments.append("\(column) = \(call)")
} else {
assignments.append("\(column) = ?")
Expand Down Expand Up @@ -175,10 +172,10 @@ enum CassandraRowWriter {
return row
}

private func functionCall(_ value: PluginCellValue) -> String? {
guard let text = value.asText?.trimmingCharacters(in: .whitespaces) else { return nil }
let lowered = text.lowercased()
return functionCalls.contains(lowered) ? lowered : nil
// The host checked a marked cell's text against the engine's vocabulary before handing it over.
private func expression(_ value: PluginCellValue, isMarked: Bool) -> String? {
guard isMarked, case .text(let cql) = value else { return nil }
return cql
}
}
}
2 changes: 1 addition & 1 deletion Plugins/CassandraDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>NSPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).CassandraPlugin</string>
<key>TableProPluginKitVersion</key>
<integer>38</integer>
<integer>39</integer>
</dict>
</plist>
Loading
Loading