Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit a138690

Browse files
committed
common: Include data diff when changing a table schema
When detecting a table creation, drop, or altering include all the table rows in the diff. Note that this still does not include modified rows of tables with an unchanged schema.
1 parent 16b736f commit a138690

File tree

3 files changed

+217
-7
lines changed

3 files changed

+217
-7
lines changed

common/diff.go

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ type SchemaDiff struct {
2222
Sql string `json:"sql"`
2323
}
2424

25+
type DataDiff struct {
26+
ActionType DiffType `json:"action_type"`
27+
Sql string `json:"sql"`
28+
Pk []DataValue `json:"pk"`
29+
}
30+
2531
type DiffObjectChangeset struct {
2632
ObjectName string `json:"object_name"`
2733
ObjectType string `json:"object_type"`
2834
Schema SchemaDiff `json:"schema"`
35+
Data []DataDiff `json:"data"`
2936
}
3037

3138
type Diffs struct {
3239
Diff []DiffObjectChangeset `json:"diff"`
40+
// TODO Add PRAGMAs here
3341
}
3442

3543
// Diff generates the differences between the two commits commitA and commitB of the two databases specified in the other parameters
@@ -128,6 +136,8 @@ func dbDiff(dbA string, dbB string) (Diffs, error) {
128136
return Diffs{}, err
129137
}
130138

139+
// TODO Check for differences in the PRAGMAs of both databases
140+
131141
// Return
132142
return diff, nil
133143
}
@@ -144,19 +154,28 @@ func diffSingleObject(sdb *sqlite.Conn, objectName string, objectType string) (b
144154
var sqlInMain, sqlInAux string
145155
err := sdb.OneValue("SELECT sql FROM main.sqlite_master WHERE name = ? AND type = ?", &sqlInMain, objectName, objectType)
146156
if err != nil && err != io.EOF { // io.EOF is okay. It is returned when the object does not exist in the main database
147-
return false, diff, err
157+
return false, DiffObjectChangeset{}, err
148158
}
149159
err = sdb.OneValue("SELECT sql FROM aux.sqlite_master WHERE name = ? AND type = ?", &sqlInAux, objectName, objectType)
150160
if err != nil && err != io.EOF { // io.EOF is okay. It is returned when the object does not exist in the aux database
151-
return false, diff, err
161+
return false, DiffObjectChangeset{}, err
152162
}
153163

154164
// Check for dropped object
155165
if sqlInMain != "" && sqlInAux == "" {
156166
diff.Schema.ActionType = ACTION_DELETE
157167
diff.Schema.Sql = "DROP " + strings.ToUpper(objectType) + " " + EscapeId(objectName) + ";"
158168

159-
// No data changes for added objects so we can return here
169+
// If this is a table, also add all the deleted data to the diff
170+
if objectType == "table" {
171+
// We never include the SQL statements because there is no need to delete all the rows when we DROP the table anyway
172+
diff.Data, err = dataDiffForAllTableRows(sdb, "main", objectName, ACTION_DELETE, false)
173+
if err != nil {
174+
return false, DiffObjectChangeset{}, err
175+
}
176+
}
177+
178+
// No further changes for dropped objects. So we can return here
160179
return true, diff, nil
161180
}
162181

@@ -165,8 +184,15 @@ func diffSingleObject(sdb *sqlite.Conn, objectName string, objectType string) (b
165184
diff.Schema.ActionType = ACTION_ADD
166185
diff.Schema.Sql = sqlInAux + ";"
167186

168-
// TODO If this is a table, also add all the data to the diff
187+
// If this is a table, also add all the added data to the diff
188+
if objectType == "table" {
189+
diff.Data, err = dataDiffForAllTableRows(sdb, "aux", objectName, ACTION_ADD, true)
190+
if err != nil {
191+
return false, DiffObjectChangeset{}, err
192+
}
193+
}
169194

195+
// No further changes for created objects. So we can return here
170196
return true, diff, nil
171197
}
172198

@@ -177,13 +203,111 @@ func diffSingleObject(sdb *sqlite.Conn, objectName string, objectType string) (b
177203

178204
// TODO If this is a table, be more clever and try to get away with ALTER TABLE instead of DROP and CREATE
179205

180-
// TODO If this is a table, also add all the data to the diff
206+
// If this is a table, also add all the data to the diff
207+
if objectType == "table" {
208+
delete_data, err := dataDiffForAllTableRows(sdb, "main", objectName, ACTION_DELETE, false)
209+
if err != nil {
210+
return false, DiffObjectChangeset{}, err
211+
}
212+
add_data, err := dataDiffForAllTableRows(sdb, "aux", objectName, ACTION_ADD, true)
213+
if err != nil {
214+
return false, DiffObjectChangeset{}, err
215+
}
216+
diff.Data = append(delete_data, add_data...)
217+
}
181218

219+
// No further changes for modified objects. So we can return here
182220
return true, diff, nil
183221
}
184222

185-
// TODO If this is a table, check for modified data
223+
// If this is a table, check for modified data
224+
if objectType == "table" {
225+
// TODO
226+
}
186227

187228
// Nothing has changed
188229
return false, diff, nil
189230
}
231+
232+
func dataDiffForAllTableRows(sdb *sqlite.Conn, schemaName string, tableName string, action DiffType, includeSql bool) (diff []DataDiff, err error) {
233+
// Retrieve a list of all primary key columns in this table
234+
pk, err := GetPrimaryKeyColumns(sdb, schemaName, tableName)
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
// Escape all the column names
240+
var pk_escaped []string
241+
for _, v := range pk {
242+
pk_escaped = append(pk_escaped, EscapeId(v))
243+
}
244+
245+
// Prepare query for the primary keys of all rows in this table. Only include the rest of the data
246+
// in the rows if required
247+
query := "SELECT " + strings.Join(pk_escaped, ",")
248+
if includeSql && action == ACTION_ADD {
249+
query += ", *"
250+
}
251+
query += " FROM " + EscapeId(schemaName) + "." + EscapeId(tableName)
252+
253+
// Retrieve data and add it to the data diff object
254+
_, _, data, err := SQLiteRunQuery(sdb, Internal, query, false, false)
255+
if err != nil {
256+
log.Printf("Error getting rows in dataDiffForAllTableRows(): %s\n", err)
257+
return nil, err
258+
}
259+
for _, row := range data.Records {
260+
var d DataDiff
261+
d.ActionType = action
262+
263+
// Prepare SQL statement when needed
264+
if includeSql {
265+
if action == ACTION_DELETE {
266+
d.Sql = "DELETE FROM " + EscapeId(tableName) + " WHERE "
267+
} else if action == ACTION_ADD {
268+
d.Sql = "INSERT INTO " + EscapeId(tableName) + " VALUES("
269+
}
270+
}
271+
272+
// Get primary key data
273+
for i := 0; i < data.ColCount; i++ {
274+
// If this column is still part of the primary key, add it to the data diff
275+
if i < len(pk) {
276+
d.Pk = append(d.Pk, row[i])
277+
}
278+
279+
// If we want to include a SQL statement for deleting data and this is still
280+
// part of the primary key, add this to the prepared DELETE statement
281+
if includeSql && action == ACTION_DELETE && i < len(pk) {
282+
d.Sql += pk_escaped[i];
283+
if row[i].Type == Null {
284+
d.Sql += " IS NULL"
285+
} else {
286+
d.Sql += "=" + EscapeValue(row[i])
287+
}
288+
d.Sql += " AND "
289+
}
290+
291+
// If we want to include a SQL statement for adding data and this is the regular
292+
// data part, add this to the prepared INSERT statement
293+
if includeSql && action == ACTION_ADD && i >= len(pk) {
294+
d.Sql += EscapeValue(row[i]) + ","
295+
}
296+
}
297+
298+
// Remove the last " AND " of the SQL query for DELETE statements and the last "," for INSERT statements
299+
// and add a semicolon instead
300+
if includeSql {
301+
if action == ACTION_DELETE {
302+
d.Sql = strings.TrimSuffix(d.Sql, " AND ") + ";"
303+
} else if action == ACTION_ADD {
304+
d.Sql = strings.TrimSuffix(d.Sql, ",") + ");"
305+
}
306+
}
307+
308+
// Add row to data diff set
309+
diff = append(diff, d)
310+
}
311+
312+
return diff, nil
313+
}

common/sqlite.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log"
88
"net/http"
99
"reflect"
10+
"sort"
1011
"strconv"
1112
"time"
1213

@@ -766,6 +767,9 @@ func SQLiteRunQuery(sdb *sqlite.Conn, querySource QuerySource, dbQuery string, i
766767
case API:
767768
row = append(row, DataValue{Name: dataRows.ColNames[i], Type: Binary,
768769
Value: base64.StdEncoding.EncodeToString(b)})
770+
case Internal:
771+
row = append(row, DataValue{Name: dataRows.ColNames[i], Type: Binary,
772+
Value: b})
769773
default:
770774
row = append(row, DataValue{Name: dataRows.ColNames[i], Type: Binary,
771775
Value: "<i>BINARY DATA</i>"})
@@ -782,7 +786,7 @@ func SQLiteRunQuery(sdb *sqlite.Conn, querySource QuerySource, dbQuery string, i
782786
if isNull && !ignoreNull {
783787
// Different sources of the query have different requirements for the output
784788
switch querySource {
785-
case API:
789+
case API, Internal:
786790
row = append(row, DataValue{Name: dataRows.ColNames[i], Type: Null})
787791
default:
788792
row = append(row, DataValue{Name: dataRows.ColNames[i], Type: Null, Value: "<i>NULL</i>"})
@@ -942,6 +946,87 @@ func Views(sdb *sqlite.Conn) (vw []string, err error) {
942946
return
943947
}
944948

949+
// Escape an identifier for safe use in SQL queries
945950
func EscapeId(id string) string {
946951
return sqlite.Mprintf("\"%w\"", id)
947952
}
953+
954+
// Format and escape a string value for use in SQL queries
955+
func EscapeValue(val DataValue) string {
956+
if val.Type == Null {
957+
return "NULL"
958+
} else if val.Type == Integer || val.Type == Float {
959+
return val.Value.(string)
960+
} else {
961+
return sqlite.Mprintf("%Q", val.Value.(string))
962+
}
963+
}
964+
965+
// Figure out the primary key columns of a table.
966+
// The schema and table parameters specify the schema and table names to use.
967+
func GetPrimaryKeyColumns(sdb *sqlite.Conn, schema string, table string) (pks []string, err error) {
968+
// Prepare query
969+
var stmt *sqlite.Stmt
970+
stmt, err = sdb.Prepare("PRAGMA " + EscapeId(schema) + ".table_info(" + EscapeId(table) + ")")
971+
if err != nil {
972+
log.Printf("Error when preparing statement in GetPrimaryKey(): %s\n", err)
973+
return nil, err
974+
}
975+
defer stmt.Finalize()
976+
977+
// Execute query and retrieve all primary key columns
978+
primaryKeyColumns := make(map[int]string)
979+
var hasColumnRowid, hasColumn_Rowid_, hasColumnOid bool
980+
err = stmt.Select(func(s *sqlite.Stmt) error {
981+
columnName, _ := s.ScanText(1)
982+
pkOrder, _, _ := s.ScanInt(5)
983+
if pkOrder > 0 {
984+
primaryKeyColumns[pkOrder] = columnName
985+
}
986+
987+
// While here check if there are any columns called rowid or similar in this table
988+
if columnName == "rowid" {
989+
hasColumnRowid = true
990+
} else if columnName == "_rowid_" {
991+
hasColumn_Rowid_ = true
992+
} else if columnName == "oid" {
993+
hasColumnOid = true
994+
}
995+
return nil
996+
})
997+
if err != nil {
998+
log.Printf("Error when retrieving rows in GetPrimaryKey(): %s\n", err)
999+
return nil, err
1000+
}
1001+
1002+
// Did we get any primary key columns? If not, this table has only an implicit primary key which
1003+
// is accessible by the name rowid, _rowid_, or oid
1004+
if len(primaryKeyColumns) > 0 {
1005+
// Explicit primary key
1006+
1007+
// Sort the columns by their order in the PK
1008+
keys := make([]int, 0, len(primaryKeyColumns))
1009+
for k := range primaryKeyColumns {
1010+
keys = append(keys, k)
1011+
}
1012+
sort.Ints(keys)
1013+
1014+
// Return columns in order
1015+
for _, k := range keys {
1016+
pks = append(pks, primaryKeyColumns[k])
1017+
}
1018+
} else {
1019+
// Implicit primary key
1020+
if !hasColumnRowid {
1021+
pks = append(pks, "rowid")
1022+
} else if !hasColumn_Rowid_ {
1023+
pks = append(pks, "_rowid_")
1024+
} else if !hasColumnOid {
1025+
pks = append(pks, "oid")
1026+
} else {
1027+
log.Printf("Unreachable rowid column in GetPrimaryKey()\n")
1028+
return nil, errors.New("Unreachable rowid column")
1029+
}
1030+
}
1031+
return pks, nil
1032+
}

common/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const (
6464
DB4S
6565
Visualisation
6666
API
67+
Internal
6768
)
6869

6970
// ************************

0 commit comments

Comments
 (0)