refactor(go): keep realtime mysql tables core only
This commit is contained in:
@@ -3,7 +3,6 @@ package realtime
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -39,8 +38,10 @@ func (w *SnapshotWriter) EnsureSchema(ctx context.Context) error {
|
|||||||
if _, err := w.exec.ExecContext(ctx, realtimeSnapshotTableSQL); err != nil {
|
if _, err := w.exec.ExecContext(ctx, realtimeSnapshotTableSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := w.exec.ExecContext(ctx, realtimeLocationTableSQL)
|
if _, err := w.exec.ExecContext(ctx, realtimeLocationTableSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return w.dropObsoleteColumns(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope) error {
|
func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope) error {
|
||||||
@@ -55,14 +56,6 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fieldsJSON, err := marshalObject(env.Fields)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
parsedJSON, err := marshalObject(env.Parsed)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
eventTime := nullableTime(env.EventTimeMS)
|
eventTime := nullableTime(env.EventTimeMS)
|
||||||
receivedAt := nullableTime(env.ReceivedAtMS)
|
receivedAt := nullableTime(env.ReceivedAtMS)
|
||||||
if _, err = w.exec.ExecContext(ctx, upsertRealtimeSnapshotSQL,
|
if _, err = w.exec.ExecContext(ctx, upsertRealtimeSnapshotSQL,
|
||||||
@@ -76,14 +69,12 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
|||||||
env.Sequence,
|
env.Sequence,
|
||||||
strings.TrimSpace(env.SourceEndpoint),
|
strings.TrimSpace(env.SourceEndpoint),
|
||||||
eventTime,
|
eventTime,
|
||||||
parsedJSON,
|
|
||||||
fieldsJSON,
|
|
||||||
receivedAt,
|
receivedAt,
|
||||||
env.StableEventID(),
|
env.StableEventID(),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
location, ok := realtimeLocationFromEnvelope(env, vehicleKey, fieldsJSON, plate)
|
location, ok := realtimeLocationFromEnvelope(env, vehicleKey, plate)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -107,7 +98,6 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
|||||||
location.DirectionDeg,
|
location.DirectionDeg,
|
||||||
location.AlarmFlag,
|
location.AlarmFlag,
|
||||||
location.StatusFlag,
|
location.StatusFlag,
|
||||||
location.FieldsJSON,
|
|
||||||
location.ReceivedAt,
|
location.ReceivedAt,
|
||||||
location.EventID,
|
location.EventID,
|
||||||
)
|
)
|
||||||
@@ -135,6 +125,47 @@ func (w *SnapshotWriter) plateForEnvelope(ctx context.Context, env envelope.Fram
|
|||||||
return strings.TrimSpace(plate), nil
|
return strings.TrimSpace(plate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *SnapshotWriter) dropObsoleteColumns(ctx context.Context) error {
|
||||||
|
queryer, ok := w.exec.(Queryer)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, column := range []struct {
|
||||||
|
table string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{table: "vehicle_realtime_snapshot", name: "parsed_json"},
|
||||||
|
{table: "vehicle_realtime_snapshot", name: "fields_json"},
|
||||||
|
{table: "vehicle_realtime_location", name: "fields_json"},
|
||||||
|
} {
|
||||||
|
exists, err := columnExists(ctx, queryer, column.table, column.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := w.exec.ExecContext(ctx, "ALTER TABLE "+column.table+" DROP COLUMN "+column.name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnExists(ctx context.Context, queryer Queryer, table string, column string) (bool, error) {
|
||||||
|
var count int
|
||||||
|
err := queryer.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = ?
|
||||||
|
AND column_name = ?`, table, column).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
type realtimeLocationRow struct {
|
type realtimeLocationRow struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
VehicleKey string
|
VehicleKey string
|
||||||
@@ -155,12 +186,11 @@ type realtimeLocationRow struct {
|
|||||||
DirectionDeg any
|
DirectionDeg any
|
||||||
AlarmFlag any
|
AlarmFlag any
|
||||||
StatusFlag any
|
StatusFlag any
|
||||||
FieldsJSON string
|
|
||||||
ReceivedAt any
|
ReceivedAt any
|
||||||
EventID string
|
EventID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string, fieldsJSON string, plate string) (realtimeLocationRow, bool) {
|
func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string, plate string) (realtimeLocationRow, bool) {
|
||||||
latitude, okLat := numberField(env.Fields, envelope.FieldLatitude)
|
latitude, okLat := numberField(env.Fields, envelope.FieldLatitude)
|
||||||
longitude, okLon := numberField(env.Fields, envelope.FieldLongitude)
|
longitude, okLon := numberField(env.Fields, envelope.FieldLongitude)
|
||||||
if !okLat || !okLon {
|
if !okLat || !okLon {
|
||||||
@@ -186,7 +216,6 @@ func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string,
|
|||||||
DirectionDeg: nullableNumberField(env.Fields, "direction_deg"),
|
DirectionDeg: nullableNumberField(env.Fields, "direction_deg"),
|
||||||
AlarmFlag: nullableNumberField(env.Fields, "alarm_flag"),
|
AlarmFlag: nullableNumberField(env.Fields, "alarm_flag"),
|
||||||
StatusFlag: nullableNumberField(env.Fields, "status_flag"),
|
StatusFlag: nullableNumberField(env.Fields, "status_flag"),
|
||||||
FieldsJSON: fieldsJSON,
|
|
||||||
ReceivedAt: nullableTime(env.ReceivedAtMS),
|
ReceivedAt: nullableTime(env.ReceivedAtMS),
|
||||||
EventID: env.StableEventID(),
|
EventID: env.StableEventID(),
|
||||||
}, true
|
}, true
|
||||||
@@ -282,17 +311,6 @@ func safeIdentifier(value string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalObject(value map[string]any) (string, error) {
|
|
||||||
if value == nil {
|
|
||||||
value = map[string]any{}
|
|
||||||
}
|
|
||||||
payload, err := json.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(payload), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullableTime(ms int64) any {
|
func nullableTime(ms int64) any {
|
||||||
if ms <= 0 {
|
if ms <= 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -322,8 +340,6 @@ const realtimeSnapshotTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_sn
|
|||||||
sequence_id INT NOT NULL DEFAULT 0,
|
sequence_id INT NOT NULL DEFAULT 0,
|
||||||
source_endpoint VARCHAR(128) NOT NULL DEFAULT '',
|
source_endpoint VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
event_time DATETIME(3) NULL,
|
event_time DATETIME(3) NULL,
|
||||||
parsed_json JSON NOT NULL,
|
|
||||||
fields_json JSON NOT NULL,
|
|
||||||
received_at DATETIME(3) NULL,
|
received_at DATETIME(3) NULL,
|
||||||
event_id VARCHAR(64) NOT NULL DEFAULT '',
|
event_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -337,8 +353,8 @@ const realtimeSnapshotTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_sn
|
|||||||
const upsertRealtimeSnapshotSQL = `
|
const upsertRealtimeSnapshotSQL = `
|
||||||
INSERT INTO vehicle_realtime_snapshot
|
INSERT INTO vehicle_realtime_snapshot
|
||||||
(protocol, vehicle_key, vin, phone, device_id, plate, message_id, sequence_id,
|
(protocol, vehicle_key, vin, phone, device_id, plate, message_id, sequence_id,
|
||||||
source_endpoint, event_time, parsed_json, fields_json, received_at, event_id)
|
source_endpoint, event_time, received_at, event_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
vin = IF(VALUES(vin) <> '', VALUES(vin), vin),
|
vin = IF(VALUES(vin) <> '', VALUES(vin), vin),
|
||||||
phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
|
phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
|
||||||
@@ -348,8 +364,6 @@ ON DUPLICATE KEY UPDATE
|
|||||||
sequence_id = VALUES(sequence_id),
|
sequence_id = VALUES(sequence_id),
|
||||||
source_endpoint = VALUES(source_endpoint),
|
source_endpoint = VALUES(source_endpoint),
|
||||||
event_time = VALUES(event_time),
|
event_time = VALUES(event_time),
|
||||||
parsed_json = VALUES(parsed_json),
|
|
||||||
fields_json = VALUES(fields_json),
|
|
||||||
received_at = VALUES(received_at),
|
received_at = VALUES(received_at),
|
||||||
event_id = VALUES(event_id),
|
event_id = VALUES(event_id),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
@@ -376,7 +390,6 @@ const realtimeLocationTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_lo
|
|||||||
direction_deg DECIMAL(10,3) NULL,
|
direction_deg DECIMAL(10,3) NULL,
|
||||||
alarm_flag BIGINT NULL,
|
alarm_flag BIGINT NULL,
|
||||||
status_flag BIGINT NULL,
|
status_flag BIGINT NULL,
|
||||||
fields_json JSON NOT NULL,
|
|
||||||
received_at DATETIME(3) NULL,
|
received_at DATETIME(3) NULL,
|
||||||
event_id VARCHAR(64) NOT NULL DEFAULT '',
|
event_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -392,9 +405,8 @@ const upsertRealtimeLocationSQL = `
|
|||||||
INSERT INTO vehicle_realtime_location
|
INSERT INTO vehicle_realtime_location
|
||||||
(protocol, vehicle_key, vin, phone, device_id, plate, message_id, sequence_id,
|
(protocol, vehicle_key, vin, phone, device_id, plate, message_id, sequence_id,
|
||||||
source_endpoint, event_time, latitude, longitude, speed_kmh, total_mileage_km,
|
source_endpoint, event_time, latitude, longitude, speed_kmh, total_mileage_km,
|
||||||
soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, fields_json,
|
soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id)
|
||||||
received_at, event_id)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
vin = IF(VALUES(vin) <> '', VALUES(vin), vin),
|
vin = IF(VALUES(vin) <> '', VALUES(vin), vin),
|
||||||
phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
|
phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
|
||||||
@@ -413,7 +425,6 @@ ON DUPLICATE KEY UPDATE
|
|||||||
direction_deg = VALUES(direction_deg),
|
direction_deg = VALUES(direction_deg),
|
||||||
alarm_flag = VALUES(alarm_flag),
|
alarm_flag = VALUES(alarm_flag),
|
||||||
status_flag = VALUES(status_flag),
|
status_flag = VALUES(status_flag),
|
||||||
fields_json = VALUES(fields_json),
|
|
||||||
received_at = VALUES(received_at),
|
received_at = VALUES(received_at),
|
||||||
event_id = VALUES(event_id),
|
event_id = VALUES(event_id),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSnapshotWriterEnsuresSchemaAndUpsertsParsedJSON(t *testing.T) {
|
func TestSnapshotWriterEnsuresSchemaAndUpsertsCoreSnapshot(t *testing.T) {
|
||||||
exec := &recordingSnapshotExec{}
|
exec := &recordingSnapshotExec{}
|
||||||
writer := NewSnapshotWriter(exec)
|
writer := NewSnapshotWriter(exec)
|
||||||
|
|
||||||
@@ -46,22 +46,26 @@ func TestSnapshotWriterEnsuresSchemaAndUpsertsParsedJSON(t *testing.T) {
|
|||||||
if !strings.Contains(exec.calls[1].query, "CREATE TABLE IF NOT EXISTS vehicle_realtime_location") {
|
if !strings.Contains(exec.calls[1].query, "CREATE TABLE IF NOT EXISTS vehicle_realtime_location") {
|
||||||
t.Fatalf("location schema query = %s", exec.calls[1].query)
|
t.Fatalf("location schema query = %s", exec.calls[1].query)
|
||||||
}
|
}
|
||||||
|
for _, call := range exec.calls[:2] {
|
||||||
|
if strings.Contains(call.query, "parsed_json") || strings.Contains(call.query, "fields_json") {
|
||||||
|
t.Fatalf("realtime schema should not contain json payload columns: %s", call.query)
|
||||||
|
}
|
||||||
|
}
|
||||||
upsert := exec.calls[2]
|
upsert := exec.calls[2]
|
||||||
if !strings.Contains(upsert.query, "ON DUPLICATE KEY UPDATE") {
|
if !strings.Contains(upsert.query, "ON DUPLICATE KEY UPDATE") {
|
||||||
t.Fatalf("upsert query = %s", upsert.query)
|
t.Fatalf("upsert query = %s", upsert.query)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(upsert.query, "parsed_json") || strings.Contains(upsert.query, "fields_json") {
|
||||||
|
t.Fatalf("snapshot upsert should not write json payload columns: %s", upsert.query)
|
||||||
|
}
|
||||||
if got, want := upsert.args[0], "GB32960"; got != want {
|
if got, want := upsert.args[0], "GB32960"; got != want {
|
||||||
t.Fatalf("protocol arg = %#v, want %q", got, want)
|
t.Fatalf("protocol arg = %#v, want %q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := upsert.args[1], "VIN001"; got != want {
|
if got, want := upsert.args[1], "VIN001"; got != want {
|
||||||
t.Fatalf("vehicle key arg = %#v, want %q", got, want)
|
t.Fatalf("vehicle key arg = %#v, want %q", got, want)
|
||||||
}
|
}
|
||||||
parsedJSON, ok := upsert.args[10].(string)
|
if len(upsert.args) != 12 {
|
||||||
if !ok {
|
t.Fatalf("snapshot upsert args = %d, want 12", len(upsert.args))
|
||||||
t.Fatalf("parsed_json arg type = %T", upsert.args[10])
|
|
||||||
}
|
|
||||||
if !strings.Contains(parsedJSON, "gd_fc_vendor_tlv") || !strings.Contains(parsedJSON, "vehicle") {
|
|
||||||
t.Fatalf("parsed_json should contain original and vendor parsed data: %s", parsedJSON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +108,9 @@ func TestSnapshotWriterUpsertsRealtimeLocationWhenCoordinatesExist(t *testing.T)
|
|||||||
if !strings.Contains(locationUpsert.query, "INSERT INTO vehicle_realtime_location") {
|
if !strings.Contains(locationUpsert.query, "INSERT INTO vehicle_realtime_location") {
|
||||||
t.Fatalf("location upsert query = %s", locationUpsert.query)
|
t.Fatalf("location upsert query = %s", locationUpsert.query)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(locationUpsert.query, "fields_json") {
|
||||||
|
t.Fatalf("location upsert should not write fields_json: %s", locationUpsert.query)
|
||||||
|
}
|
||||||
if got, want := locationUpsert.args[0], "JT808"; got != want {
|
if got, want := locationUpsert.args[0], "JT808"; got != want {
|
||||||
t.Fatalf("protocol arg = %#v, want %q", got, want)
|
t.Fatalf("protocol arg = %#v, want %q", got, want)
|
||||||
}
|
}
|
||||||
@@ -125,6 +132,9 @@ func TestSnapshotWriterUpsertsRealtimeLocationWhenCoordinatesExist(t *testing.T)
|
|||||||
if got, want := locationUpsert.args[14], 88.0; got != want {
|
if got, want := locationUpsert.args[14], 88.0; got != want {
|
||||||
t.Fatalf("soc arg = %#v, want %v", got, want)
|
t.Fatalf("soc arg = %#v, want %v", got, want)
|
||||||
}
|
}
|
||||||
|
if len(locationUpsert.args) != 21 {
|
||||||
|
t.Fatalf("location upsert args = %d, want 21", len(locationUpsert.args))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSnapshotWriterBackfillsPlateFromBindingByVIN(t *testing.T) {
|
func TestSnapshotWriterBackfillsPlateFromBindingByVIN(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user