forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db
2
3import (
4 "crypto/sha1"
5 "database/sql"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "maps"
10 "slices"
11 "strings"
12 "time"
13
14 "github.com/bluesky-social/indigo/atproto/syntax"
15 "tangled.sh/tangled.sh/core/api/tangled"
16 "tangled.sh/tangled.sh/core/consts"
17)
18
19type ConcreteType string
20
21const (
22 ConcreteTypeNull ConcreteType = "null"
23 ConcreteTypeString ConcreteType = "string"
24 ConcreteTypeInt ConcreteType = "integer"
25 ConcreteTypeBool ConcreteType = "boolean"
26)
27
28type ValueTypeFormat string
29
30const (
31 ValueTypeFormatAny ValueTypeFormat = "any"
32 ValueTypeFormatDid ValueTypeFormat = "did"
33)
34
35// ValueType represents an atproto lexicon type definition with constraints
36type ValueType struct {
37 Type ConcreteType `json:"type"`
38 Format ValueTypeFormat `json:"format,omitempty"`
39 Enum []string `json:"enum,omitempty"`
40}
41
42func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
43 return tangled.LabelDefinition_ValueType{
44 Type: string(vt.Type),
45 Format: string(vt.Format),
46 Enum: vt.Enum,
47 }
48}
49
50func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
51 return ValueType{
52 Type: ConcreteType(record.Type),
53 Format: ValueTypeFormat(record.Format),
54 Enum: record.Enum,
55 }
56}
57
58func (vt ValueType) IsConcreteType() bool {
59 return vt.Type == ConcreteTypeNull ||
60 vt.Type == ConcreteTypeString ||
61 vt.Type == ConcreteTypeInt ||
62 vt.Type == ConcreteTypeBool
63}
64
65func (vt ValueType) IsNull() bool {
66 return vt.Type == ConcreteTypeNull
67}
68
69func (vt ValueType) IsString() bool {
70 return vt.Type == ConcreteTypeString
71}
72
73func (vt ValueType) IsInt() bool {
74 return vt.Type == ConcreteTypeInt
75}
76
77func (vt ValueType) IsBool() bool {
78 return vt.Type == ConcreteTypeBool
79}
80
81func (vt ValueType) IsEnum() bool {
82 return len(vt.Enum) > 0
83}
84
85func (vt ValueType) IsDidFormat() bool {
86 return vt.Format == ValueTypeFormatDid
87}
88
89func (vt ValueType) IsAnyFormat() bool {
90 return vt.Format == ValueTypeFormatAny
91}
92
93type LabelDefinition struct {
94 Id int64
95 Did string
96 Rkey string
97
98 Name string
99 ValueType ValueType
100 Scope []string
101 Color *string
102 Multiple bool
103 Created time.Time
104}
105
106func (l *LabelDefinition) AtUri() syntax.ATURI {
107 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
108}
109
110func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
111 vt := l.ValueType.AsRecord()
112 return tangled.LabelDefinition{
113 Name: l.Name,
114 Color: l.Color,
115 CreatedAt: l.Created.Format(time.RFC3339),
116 Multiple: &l.Multiple,
117 Scope: l.Scope,
118 ValueType: &vt,
119 }
120}
121
122// random color for a given seed
123func randomColor(seed string) string {
124 hash := sha1.Sum([]byte(seed))
125 hexStr := hex.EncodeToString(hash[:])
126 r := hexStr[0:2]
127 g := hexStr[2:4]
128 b := hexStr[4:6]
129
130 return fmt.Sprintf("#%s%s%s", r, g, b)
131}
132
133func (ld LabelDefinition) GetColor() string {
134 if ld.Color == nil {
135 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
136 color := randomColor(seed)
137 return color
138 }
139
140 return *ld.Color
141}
142
143func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
144 created, err := time.Parse(time.RFC3339, record.CreatedAt)
145 if err != nil {
146 created = time.Now()
147 }
148
149 multiple := false
150 if record.Multiple != nil {
151 multiple = *record.Multiple
152 }
153
154 var vt ValueType
155 if record.ValueType != nil {
156 vt = ValueTypeFromRecord(*record.ValueType)
157 }
158
159 return &LabelDefinition{
160 Did: did,
161 Rkey: rkey,
162
163 Name: record.Name,
164 ValueType: vt,
165 Scope: record.Scope,
166 Color: record.Color,
167 Multiple: multiple,
168 Created: created,
169 }, nil
170}
171
172func DeleteLabelDefinition(e Execer, filters ...filter) error {
173 var conditions []string
174 var args []any
175 for _, filter := range filters {
176 conditions = append(conditions, filter.Condition())
177 args = append(args, filter.Arg()...)
178 }
179 whereClause := ""
180 if conditions != nil {
181 whereClause = " where " + strings.Join(conditions, " and ")
182 }
183 query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
184 _, err := e.Exec(query, args...)
185 return err
186}
187
188// no updating type for now
189func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) {
190 result, err := e.Exec(
191 `insert into label_definitions (
192 did,
193 rkey,
194 name,
195 value_type,
196 value_format,
197 value_enum,
198 scope,
199 color,
200 multiple,
201 created
202 )
203 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
204 on conflict(did, rkey) do update set
205 name = excluded.name,
206 scope = excluded.scope,
207 color = excluded.color,
208 multiple = excluded.multiple`,
209 l.Did,
210 l.Rkey,
211 l.Name,
212 l.ValueType.Type,
213 l.ValueType.Format,
214 strings.Join(l.ValueType.Enum, ","),
215 strings.Join(l.Scope, ","),
216 l.Color,
217 l.Multiple,
218 l.Created.Format(time.RFC3339),
219 time.Now().Format(time.RFC3339),
220 )
221 if err != nil {
222 return 0, err
223 }
224
225 id, err := result.LastInsertId()
226 if err != nil {
227 return 0, err
228 }
229
230 l.Id = id
231
232 return id, nil
233}
234
235func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) {
236 var labelDefinitions []LabelDefinition
237 var conditions []string
238 var args []any
239
240 for _, filter := range filters {
241 conditions = append(conditions, filter.Condition())
242 args = append(args, filter.Arg()...)
243 }
244
245 whereClause := ""
246 if conditions != nil {
247 whereClause = " where " + strings.Join(conditions, " and ")
248 }
249
250 query := fmt.Sprintf(
251 `
252 select
253 id,
254 did,
255 rkey,
256 name,
257 value_type,
258 value_format,
259 value_enum,
260 scope,
261 color,
262 multiple,
263 created
264 from label_definitions
265 %s
266 order by created
267 `,
268 whereClause,
269 )
270
271 rows, err := e.Query(query, args...)
272 if err != nil {
273 return nil, err
274 }
275 defer rows.Close()
276
277 for rows.Next() {
278 var labelDefinition LabelDefinition
279 var createdAt, enumVariants, scopes string
280 var color sql.Null[string]
281 var multiple int
282
283 if err := rows.Scan(
284 &labelDefinition.Id,
285 &labelDefinition.Did,
286 &labelDefinition.Rkey,
287 &labelDefinition.Name,
288 &labelDefinition.ValueType.Type,
289 &labelDefinition.ValueType.Format,
290 &enumVariants,
291 &scopes,
292 &color,
293 &multiple,
294 &createdAt,
295 ); err != nil {
296 return nil, err
297 }
298
299 labelDefinition.Created, err = time.Parse(time.RFC3339, createdAt)
300 if err != nil {
301 labelDefinition.Created = time.Now()
302 }
303
304 if color.Valid {
305 labelDefinition.Color = &color.V
306 }
307
308 if multiple != 0 {
309 labelDefinition.Multiple = true
310 }
311
312 if enumVariants != "" {
313 labelDefinition.ValueType.Enum = strings.Split(enumVariants, ",")
314 }
315
316 for s := range strings.SplitSeq(scopes, ",") {
317 labelDefinition.Scope = append(labelDefinition.Scope, s)
318 }
319
320 labelDefinitions = append(labelDefinitions, labelDefinition)
321 }
322
323 return labelDefinitions, nil
324}
325
326// helper to get exactly one label def
327func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) {
328 labels, err := GetLabelDefinitions(e, filters...)
329 if err != nil {
330 return nil, err
331 }
332
333 if labels == nil {
334 return nil, sql.ErrNoRows
335 }
336
337 if len(labels) != 1 {
338 return nil, fmt.Errorf("too many rows returned")
339 }
340
341 return &labels[0], nil
342}
343
344type LabelOp struct {
345 Id int64
346 Did string
347 Rkey string
348 Subject syntax.ATURI
349 Operation LabelOperation
350 OperandKey string
351 OperandValue string
352 PerformedAt time.Time
353 IndexedAt time.Time
354}
355
356func (l LabelOp) SortAt() time.Time {
357 createdAt := l.PerformedAt
358 indexedAt := l.IndexedAt
359
360 // if we don't have an indexedat, fall back to now
361 if indexedAt.IsZero() {
362 indexedAt = time.Now()
363 }
364
365 // if createdat is invalid (before epoch), treat as null -> return zero time
366 if createdAt.Before(time.UnixMicro(0)) {
367 return time.Time{}
368 }
369
370 // if createdat is <= indexedat, use createdat
371 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
372 return createdAt
373 }
374
375 // otherwise, createdat is in the future relative to indexedat -> use indexedat
376 return indexedAt
377}
378
379type LabelOperation string
380
381const (
382 LabelOperationAdd LabelOperation = "add"
383 LabelOperationDel LabelOperation = "del"
384)
385
386// a record can create multiple label ops
387func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
388 performed, err := time.Parse(time.RFC3339, record.PerformedAt)
389 if err != nil {
390 performed = time.Now()
391 }
392
393 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
394 return LabelOp{
395 Did: did,
396 Rkey: rkey,
397 Subject: syntax.ATURI(record.Subject),
398 OperandKey: operand.Key,
399 OperandValue: operand.Value,
400 PerformedAt: performed,
401 }
402 }
403
404 var ops []LabelOp
405 for _, o := range record.Add {
406 if o != nil {
407 op := mkOp(o)
408 op.Operation = LabelOperationAdd
409 ops = append(ops, op)
410 }
411 }
412 for _, o := range record.Delete {
413 if o != nil {
414 op := mkOp(o)
415 op.Operation = LabelOperationDel
416 ops = append(ops, op)
417 }
418 }
419
420 return ops
421}
422
423func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
424 if len(ops) == 0 {
425 return tangled.LabelOp{}
426 }
427
428 // use the first operation to establish common fields
429 first := ops[0]
430 record := tangled.LabelOp{
431 Subject: string(first.Subject),
432 PerformedAt: first.PerformedAt.Format(time.RFC3339),
433 }
434
435 var addOperands []*tangled.LabelOp_Operand
436 var deleteOperands []*tangled.LabelOp_Operand
437
438 for _, op := range ops {
439 operand := &tangled.LabelOp_Operand{
440 Key: op.OperandKey,
441 Value: op.OperandValue,
442 }
443
444 switch op.Operation {
445 case LabelOperationAdd:
446 addOperands = append(addOperands, operand)
447 case LabelOperationDel:
448 deleteOperands = append(deleteOperands, operand)
449 default:
450 return tangled.LabelOp{}
451 }
452 }
453
454 record.Add = addOperands
455 record.Delete = deleteOperands
456
457 return record
458}
459
460func AddLabelOp(e Execer, l *LabelOp) (int64, error) {
461 now := time.Now()
462 result, err := e.Exec(
463 `insert into label_ops (
464 did,
465 rkey,
466 subject,
467 operation,
468 operand_key,
469 operand_value,
470 performed,
471 indexed
472 )
473 values (?, ?, ?, ?, ?, ?, ?, ?)
474 on conflict(did, rkey, subject, operand_key, operand_value) do update set
475 operation = excluded.operation,
476 operand_value = excluded.operand_value,
477 performed = excluded.performed,
478 indexed = excluded.indexed`,
479 l.Did,
480 l.Rkey,
481 l.Subject.String(),
482 string(l.Operation),
483 l.OperandKey,
484 l.OperandValue,
485 l.PerformedAt.Format(time.RFC3339),
486 now.Format(time.RFC3339),
487 )
488 if err != nil {
489 return 0, err
490 }
491
492 id, err := result.LastInsertId()
493 if err != nil {
494 return 0, err
495 }
496
497 l.Id = id
498 l.IndexedAt = now
499
500 return id, nil
501}
502
503func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) {
504 var labelOps []LabelOp
505 var conditions []string
506 var args []any
507
508 for _, filter := range filters {
509 conditions = append(conditions, filter.Condition())
510 args = append(args, filter.Arg()...)
511 }
512
513 whereClause := ""
514 if conditions != nil {
515 whereClause = " where " + strings.Join(conditions, " and ")
516 }
517
518 query := fmt.Sprintf(
519 `
520 select
521 id,
522 did,
523 rkey,
524 subject,
525 operation,
526 operand_key,
527 operand_value,
528 performed,
529 indexed
530 from label_ops
531 %s
532 order by indexed
533 `,
534 whereClause,
535 )
536
537 rows, err := e.Query(query, args...)
538 if err != nil {
539 return nil, err
540 }
541 defer rows.Close()
542
543 for rows.Next() {
544 var labelOp LabelOp
545 var performedAt, indexedAt string
546
547 if err := rows.Scan(
548 &labelOp.Id,
549 &labelOp.Did,
550 &labelOp.Rkey,
551 &labelOp.Subject,
552 &labelOp.Operation,
553 &labelOp.OperandKey,
554 &labelOp.OperandValue,
555 &performedAt,
556 &indexedAt,
557 ); err != nil {
558 return nil, err
559 }
560
561 labelOp.PerformedAt, err = time.Parse(time.RFC3339, performedAt)
562 if err != nil {
563 labelOp.PerformedAt = time.Now()
564 }
565
566 labelOp.IndexedAt, err = time.Parse(time.RFC3339, indexedAt)
567 if err != nil {
568 labelOp.IndexedAt = time.Now()
569 }
570
571 labelOps = append(labelOps, labelOp)
572 }
573
574 return labelOps, nil
575}
576
577// get labels for a given list of subject URIs
578func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) {
579 ops, err := GetLabelOps(e, filters...)
580 if err != nil {
581 return nil, err
582 }
583
584 // group ops by subject
585 opsBySubject := make(map[syntax.ATURI][]LabelOp)
586 for _, op := range ops {
587 subject := syntax.ATURI(op.Subject)
588 opsBySubject[subject] = append(opsBySubject[subject], op)
589 }
590
591 // get all unique labelats for creating the context
592 labelAtSet := make(map[string]bool)
593 for _, op := range ops {
594 labelAtSet[op.OperandKey] = true
595 }
596 labelAts := slices.Collect(maps.Keys(labelAtSet))
597
598 actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
599 if err != nil {
600 return nil, err
601 }
602
603 // apply label ops for each subject and collect results
604 results := make(map[syntax.ATURI]LabelState)
605 for subject, subjectOps := range opsBySubject {
606 state := NewLabelState()
607 actx.ApplyLabelOps(state, subjectOps)
608 results[subject] = state
609 }
610
611 return results, nil
612}
613
614type set = map[string]struct{}
615
616type LabelState struct {
617 inner map[string]set
618}
619
620func NewLabelState() LabelState {
621 return LabelState{
622 inner: make(map[string]set),
623 }
624}
625
626func (s LabelState) Inner() map[string]set {
627 return s.inner
628}
629
630func (s LabelState) ContainsLabel(l string) bool {
631 if valset, exists := s.inner[l]; exists {
632 if valset != nil {
633 return true
634 }
635 }
636
637 return false
638}
639
640// go maps behavior in templates make this necessary,
641// indexing a map and getting `set` in return is apparently truthy
642func (s LabelState) ContainsLabelAndVal(l, v string) bool {
643 if valset, exists := s.inner[l]; exists {
644 if _, exists := valset[v]; exists {
645 return true
646 }
647 }
648
649 return false
650}
651
652func (s LabelState) GetValSet(l string) set {
653 if valset, exists := s.inner[l]; exists {
654 return valset
655 } else {
656 return make(set)
657 }
658}
659
660type LabelApplicationCtx struct {
661 Defs map[string]*LabelDefinition // labelAt -> labelDef
662}
663
664var (
665 LabelNoOpError = errors.New("no-op")
666)
667
668func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) {
669 labels, err := GetLabelDefinitions(e, filters...)
670 if err != nil {
671 return nil, err
672 }
673
674 defs := make(map[string]*LabelDefinition)
675 for _, l := range labels {
676 defs[l.AtUri().String()] = &l
677 }
678
679 return &LabelApplicationCtx{defs}, nil
680}
681
682func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
683 def, ok := c.Defs[op.OperandKey]
684 if !ok {
685 // this def was deleted, but an op exists, so we just skip over the op
686 return nil
687 }
688
689 switch op.Operation {
690 case LabelOperationAdd:
691 // if valueset is empty, init it
692 if state.inner[op.OperandKey] == nil {
693 state.inner[op.OperandKey] = make(set)
694 }
695
696 // if valueset is populated & this val alr exists, this labelop is a noop
697 if valueSet, exists := state.inner[op.OperandKey]; exists {
698 if _, exists = valueSet[op.OperandValue]; exists {
699 return LabelNoOpError
700 }
701 }
702
703 if def.Multiple {
704 // append to set
705 state.inner[op.OperandKey][op.OperandValue] = struct{}{}
706 } else {
707 // reset to just this value
708 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
709 }
710
711 case LabelOperationDel:
712 // if label DNE, then deletion is a no-op
713 if valueSet, exists := state.inner[op.OperandKey]; !exists {
714 return LabelNoOpError
715 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
716 return LabelNoOpError
717 }
718
719 if def.Multiple {
720 // remove from set
721 delete(state.inner[op.OperandKey], op.OperandValue)
722 } else {
723 // reset the entire label
724 delete(state.inner, op.OperandKey)
725 }
726
727 // if the map becomes empty, then set it to nil, this is just the inverse of add
728 if len(state.inner[op.OperandKey]) == 0 {
729 state.inner[op.OperandKey] = nil
730 }
731
732 }
733
734 return nil
735}
736
737func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
738 // sort label ops in sort order first
739 slices.SortFunc(ops, func(a, b LabelOp) int {
740 return a.SortAt().Compare(b.SortAt())
741 })
742
743 // apply ops in sequence
744 for _, o := range ops {
745 _ = c.ApplyLabelOp(state, o)
746 }
747}
748
749// IsInverse checks if one label operation is the inverse of another
750// returns true if one is an add and the other is a delete with the same key and value
751func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
752 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
753 return false
754 }
755
756 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
757 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
758}
759
760// removes pairs of label operations that are inverses of each other
761// from the given slice. the function preserves the order of remaining operations.
762func ReduceLabelOps(ops []LabelOp) []LabelOp {
763 if len(ops) <= 1 {
764 return ops
765 }
766
767 keep := make([]bool, len(ops))
768 for i := range keep {
769 keep[i] = true
770 }
771
772 for i := range ops {
773 if !keep[i] {
774 continue
775 }
776
777 for j := i + 1; j < len(ops); j++ {
778 if !keep[j] {
779 continue
780 }
781
782 if ops[i].IsInverse(ops[j]) {
783 keep[i] = false
784 keep[j] = false
785 break // move to next i since this one is now eliminated
786 }
787 }
788 }
789
790 // build result slice with only kept operations
791 var result []LabelOp
792 for i, op := range ops {
793 if keep[i] {
794 result = append(result, op)
795 }
796 }
797
798 return result
799}
800
801func DefaultLabelDefs() []string {
802 rkeys := []string{
803 "wontfix",
804 "duplicate",
805 "assignee",
806 "good-first-issue",
807 "documentation",
808 }
809
810 defs := make([]string, len(rkeys))
811 for i, r := range rkeys {
812 defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
813 }
814
815 return defs
816}