Skip to content

Commit 780d2ff

Browse files
committed
Address Codex review: .schema indexes/triggers, dump REAL literals
- .schema TABLE now includes the table's indexes/triggers (tbl_name match), matching the .dump fix. - .dump renders non-finite REALs as restorable literals (9e999 / -9e999; NaN -> NULL) instead of "inf", which SQLite would parse as an identifier. - Document two known limitations: .dump isn't FK-dependency-ordered (and foreign_keys can't be toggled in the sandbox), and ALTER TABLE RENAME into the reserved _audit namespace isn't caught (SQLite doesn't expose the new name to the authorizer; low severity since the audit trail is external). https://claude.ai/code/session_01AdvNtQ7wCMVy8etacVQbjg
1 parent b07abb5 commit 780d2ff

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

Sources/SwiftSQLiteBash/DotCommands.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ enum DotCommandRunner {
5353
var sql = "SELECT sql FROM sqlite_schema WHERE sql IS NOT NULL "
5454
+ "AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'"
5555
if let name = args.first {
56-
sql += " AND name='\(escapeSQLString(name))'"
56+
// Include the table's own indexes/triggers (tbl_name match),
57+
// not just the object literally named NAME.
58+
let escaped = escapeSQLString(name)
59+
sql += " AND (name='\(escaped)' OR tbl_name='\(escaped)')"
5760
}
5861
sql += " ORDER BY (type='table') DESC, name;"
5962
return await emitSchemaSQL(connection, sql)
@@ -135,6 +138,12 @@ enum DotCommandRunner {
135138
// No `PRAGMA foreign_keys=OFF;` here: replaying the dump through
136139
// this sandboxed command would hit the all-PRAGMA-denied
137140
// authorizer. The whole restore runs in one transaction instead.
141+
//
142+
// Limitation: rows are emitted in table-name order, not foreign-key
143+
// dependency order, and foreign_keys can't be toggled off in the
144+
// sandbox — so restoring a dump of FK-constrained tables can fail
145+
// when a child row precedes its parent. Adequate for inspection;
146+
// a dependency-ordered dump is future work.
138147
var out = "BEGIN TRANSACTION;\n"
139148
var schemaSQL = "SELECT type, name, sql FROM sqlite_schema "
140149
+ "WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'"
@@ -331,7 +340,13 @@ func sqlLiteral(_ value: SQLiteValue) -> String {
331340
switch value {
332341
case .null: return "NULL"
333342
case .integer(let i): return String(i)
334-
case .real(let d): return String(d)
343+
case .real(let d):
344+
// String(infinity) is "inf", which SQLite parses as an identifier,
345+
// not a number — emit a restorable numeric literal instead. (SQLite
346+
// normalizes NaN to NULL on storage, so NaN shouldn't occur.)
347+
if d.isFinite { return String(d) }
348+
if d.isNaN { return "NULL" }
349+
return d > 0 ? "9e999" : "-9e999"
335350
case .text(let s): return "'\(escapeSQLString(s))'"
336351
case .blob(let data): return blobLiteral(data)
337352
}

Sources/SwiftSQLiteKit/EngineContext.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ final class EngineContext: @unchecked Sendable {
110110
private func guardedObjectName(action: Int32, arg1: String?, arg2: String?) -> String? {
111111
switch action {
112112
case SQLITE_ALTER_TABLE:
113+
// arg2 is the table being altered (its OLD name on a RENAME).
114+
// Limitation: SQLite doesn't pass the NEW name to the authorizer,
115+
// so `ALTER TABLE t RENAME TO _audit_x` isn't caught by the
116+
// reserved-prefix guard. Low severity — the audit trail is an
117+
// external file, not an in-DB `_audit*` table, so the reserved
118+
// namespace is defense-in-depth only.
113119
return arg2
114120
case SQLITE_CREATE_INDEX, SQLITE_CREATE_TEMP_INDEX,
115121
SQLITE_DROP_INDEX, SQLITE_DROP_TEMP_INDEX,

0 commit comments

Comments
 (0)