Skip to content

Commit a7687fa

Browse files
committed
Track triggers and indices in schema
1 parent c308ab2 commit a7687fa

6 files changed

Lines changed: 142 additions & 10 deletions

File tree

Sources/Compiler/Driver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
public actor Driver {
1111
private let fileSystem: FileSystem
1212
private var results: [Path: Output] = [:]
13-
private var currentSchema: Schema = [:]
13+
private var currentSchema = Schema()
1414

1515
public typealias Path = String
1616

Sources/Compiler/Gen/Language.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ extension Language {
6565
tables: [GeneratedModel],
6666
queries: [(String?, [GeneratedQuery])]
6767
) {
68-
let tables = schema.mapValues(model(for:))
68+
let tables = schema.tables.mapValues(model(for:))
6969
let queries = queries.map { ($0.map { "\($0)Queries" }, $1.map{ query(for: $0, tables: tables) }) }
7070
return (Array(tables.values), queries)
7171
}

Sources/Compiler/Schema.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,26 @@
77

88
import OrderedCollections
99

10-
public typealias Schema = OrderedDictionary<Substring, Table>
10+
public struct Schema {
11+
public var tables: OrderedDictionary<Substring, Table> = [:]
12+
public var triggers: OrderedDictionary<Substring, Trigger> = [:]
13+
public var indices: OrderedDictionary<Substring, Index> = [:]
14+
15+
public subscript(tableName: Substring) -> Table? {
16+
_read { yield tables[tableName] }
17+
_modify { yield &tables[tableName] }
18+
}
19+
20+
public subscript(trigger triggerName: Substring) -> Trigger? {
21+
_read { yield triggers[triggerName] }
22+
_modify { yield &triggers[triggerName] }
23+
}
24+
25+
public subscript(index indexName: Substring) -> Index? {
26+
_read { yield indices[indexName] }
27+
_modify { yield &indices[indexName] }
28+
}
29+
}
1130

1231
// TODO: An ordered dictionary may not be the best representation of the
1332
// TODO: columns. Since this is used even in selects, the user could
@@ -39,3 +58,17 @@ public struct Table: Sendable {
3958
return .row(.named(columns))
4059
}
4160
}
61+
62+
public struct Trigger {
63+
/// The name of the trigger
64+
public let name: Substring
65+
/// The table the trigger is watching
66+
public let targetTable: Substring
67+
/// Any table accessed in the `BEGIN/END`
68+
public let usedTables: Set<Substring>
69+
}
70+
71+
public struct Index {
72+
public let name: Substring
73+
public let table: Substring
74+
}

Sources/Compiler/Sema/StmtTypeChecker.swift

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,24 @@ extension StmtTypeChecker: StmtSyntaxVisitor {
169169
_ = typeCheck(whereExpr)
170170
}
171171

172+
schema[index: stmt.name.value] = Index(
173+
name: stmt.name.value,
174+
table: table.name
175+
)
176+
172177
return .empty
173178
}
174179

175180
mutating func visit(_ stmt: borrowing DropIndexStmtSyntax) -> ResultColumns {
176-
// Indices are not stored at the moment, so there is nothign to do.
181+
if !stmt.ifExists, schema[index: stmt.name.value] == nil {
182+
diagnostics.add(.init("Index does not exist", at: stmt.name.location))
183+
}
184+
185+
schema[index: stmt.name.value] = nil
177186
return .empty
178187
}
179188

180189
mutating func visit(_ stmt: borrowing ReindexStmtSyntax) -> ResultColumns {
181-
// Indices are not stored at the moment, so there is nothign to do.
182-
// We cant really even validate the name since it can be the
183-
// index name and not just the table
184190
return .empty
185191
}
186192

@@ -246,6 +252,13 @@ extension StmtTypeChecker: StmtSyntaxVisitor {
246252
return .empty
247253
}
248254

255+
if !stmt.ifNotExists, schema[trigger: stmt.triggerName.value] != nil {
256+
diagnostics.add(.init(
257+
"Trigger with name already exists",
258+
at: stmt.triggerName.location
259+
))
260+
}
261+
249262
switch stmt.action {
250263
case .delete:
251264
insertTableAndColumnsIntoEnv(table, as: "old", globallyAddColumns: false)
@@ -279,11 +292,21 @@ extension StmtTypeChecker: StmtSyntaxVisitor {
279292
}
280293
}
281294

295+
schema[trigger: stmt.triggerName.value] = Trigger(
296+
name: stmt.triggerName.value,
297+
targetTable: table.name,
298+
usedTables: usedTableNames
299+
)
300+
282301
return .empty
283302
}
284303

285-
func visit(_ stmt: borrowing DropTriggerStmtSyntax) -> ResultColumns {
286-
// TODO: Track triggers
304+
mutating func visit(_ stmt: borrowing DropTriggerStmtSyntax) -> ResultColumns {
305+
if !stmt.ifExists, schema[trigger: stmt.triggerName.value] == nil {
306+
diagnostics.add(.init("Trigger with name does not exist", at: stmt.triggerName.location))
307+
}
308+
309+
schema[trigger: stmt.triggerName.value] = nil
287310
return .empty
288311
}
289312
}
@@ -942,6 +965,24 @@ extension StmtTypeChecker {
942965
diagnostics.add(.tableDoesNotExist(dropTable.tableName.name))
943966
}
944967

968+
for trigger in schema.triggers.values {
969+
if trigger.targetTable == dropTable.tableName.name.value {
970+
// Dropping a table automatically removes any trigger its the target of.
971+
schema[trigger: trigger.name] = nil
972+
} else {
973+
guard !trigger.usedTables.contains(dropTable.tableName.name.value) else { continue }
974+
975+
// SQLite seemingly from my tests will allow this to happen but I swear I've
976+
// had errors from it before. But error if the table is used in a trigger.
977+
// Any trigger where its the target table will automatically be deleted
978+
// so those dont matter
979+
diagnostics.add(.init(
980+
"Table referenced in statements of trigger '\(trigger.name)'",
981+
at: dropTable.location
982+
))
983+
}
984+
}
985+
945986
schema[dropTable.tableName.name.value] = nil
946987
}
947988

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
CREATE TABLE foo (bar INTEGER);
2+
CREATE TABLE baz (qux INTEGER);
3+
4+
CREATE TRIGGER fooUpdate
5+
AFTER UPDATE ON foo
6+
BEGIN
7+
INSERT INTO baz (qux) VALUES (1);
8+
END;
9+
10+
-- CHECK-ERROR: Trigger with name already exists
11+
CREATE TRIGGER fooUpdate
12+
AFTER UPDATE ON foo
13+
BEGIN
14+
INSERT INTO baz (qux) VALUES (1);
15+
END;
16+
17+
-- CHECK-ERROR: Trigger with name does not exist
18+
DROP TRIGGER doesNotExist;
19+
20+
DROP TRIGGER IF EXISTS doesNotExistButItDoesntMatter;
21+
22+
-- CHECK-ERROR: Table referenced in statements of trigger 'fooUpdate'
23+
DROP TABLE baz;
24+
25+
DROP TABLE foo;
26+
27+
-- CHECK-ERROR: Trigger with name does not exist
28+
DROP TRIGGER fooUpdate;

Tests/CompilerTests/CompilerTests.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ class CompilerTests: XCTestCase {
6464
}
6565
)
6666
}
67+
68+
func testTriggers() throws {
69+
try checkErrors(compile: "CompileTriggers", dump: true)
70+
}
6771
}
6872

6973
struct CheckSignature: Checkable {
@@ -105,7 +109,7 @@ func checkSchema(
105109
validator: IsAlwaysValid(),
106110
context: "tests"
107111
)
108-
return (Array(compiler.schema.values), diags)
112+
return (Array(compiler.schema.tables.values), diags)
109113
},
110114
prefix: prefix,
111115
dump: dump,
@@ -178,3 +182,29 @@ func checkWithErrors<Output>(
178182
line: line
179183
)
180184
}
185+
186+
func checkErrors(
187+
compile sqlFile: String,
188+
prefix: String = "CHECK-ERROR",
189+
dump: Bool = false,
190+
file: StaticString = #filePath,
191+
line: UInt = #line
192+
) throws {
193+
try check(
194+
sqlFile: sqlFile,
195+
parse: { contents in
196+
var compiler = Compiler()
197+
let (_, d) = compiler.compile(
198+
source: contents,
199+
validator: IsAlwaysValid(),
200+
context: "tests"
201+
)
202+
203+
return d.map(\.message)
204+
},
205+
prefix: prefix,
206+
dump: dump,
207+
file: file,
208+
line: line
209+
)
210+
}

0 commit comments

Comments
 (0)