|
|
@@ -95,7 +95,60 @@ func initSchema() {
|
|
|
_, _ = TD.Exec("ALTER STABLE readings MODIFY TAG metric BINARY(256)")
|
|
|
}
|
|
|
|
|
|
-// InsertReading inserts a single reading
|
|
|
+// ReadingRecord represents a single reading to be batch-inserted
|
|
|
+type ReadingRecord struct {
|
|
|
+ DeviceID string
|
|
|
+ Metric string
|
|
|
+ Value float64
|
|
|
+ LocationID string
|
|
|
+ Ts time.Time
|
|
|
+}
|
|
|
+
|
|
|
+// escapeSQL escapes single quotes for SQL string literals to prevent injection
|
|
|
+func escapeSQL(s string) string {
|
|
|
+ return strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "'", "''")
|
|
|
+}
|
|
|
+
|
|
|
+// BatchInsertReadings batch inserts multiple readings in one REST call, reducing I/O and network round-trips.
|
|
|
+// Splits into chunks of batchSize (default 500) if records exceed limit to avoid oversized requests.
|
|
|
+func BatchInsertReadings(records []ReadingRecord) error {
|
|
|
+ if TD == nil {
|
|
|
+ return fmt.Errorf("TDengine not initialized")
|
|
|
+ }
|
|
|
+ if len(records) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ const batchSize = 500
|
|
|
+ tsFmt := "2006-01-02 15:04:05.000"
|
|
|
+
|
|
|
+ for start := 0; start < len(records); start += batchSize {
|
|
|
+ end := start + batchSize
|
|
|
+ if end > len(records) {
|
|
|
+ end = len(records)
|
|
|
+ }
|
|
|
+ chunk := records[start:end]
|
|
|
+
|
|
|
+ var sb strings.Builder
|
|
|
+ for i, r := range chunk {
|
|
|
+ safeMetric := strings.ReplaceAll(strings.ReplaceAll(r.Metric, ".", "_"), "-", "_")
|
|
|
+ safeDID := fmt.Sprintf("d_%s_%s", strings.ReplaceAll(r.DeviceID, "-", "_"), safeMetric)
|
|
|
+ sb.WriteString(fmt.Sprintf("INSERT INTO `%s` USING readings TAGS ('%s', '%s', '%s') VALUES ('%s', %f)",
|
|
|
+ safeDID, escapeSQL(r.DeviceID), escapeSQL(r.Metric), escapeSQL(r.LocationID),
|
|
|
+ r.Ts.Format(tsFmt), r.Value))
|
|
|
+ if i < len(chunk)-1 {
|
|
|
+ sb.WriteString("; ")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := TD.Exec(sb.String()); err != nil {
|
|
|
+ return fmt.Errorf("BatchInsertReadings chunk [%d:%d]: %w", start, end, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// InsertReading inserts a single reading (kept for backward compatibility)
|
|
|
func InsertReading(deviceID string, metric string, val float64, locationID string, ts time.Time) error {
|
|
|
if TD == nil {
|
|
|
return fmt.Errorf("TDengine not initialized")
|