kinship 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +233 -0
- data/Rakefile +8 -0
- data/lib/kinship/graph.rb +145 -0
- data/lib/kinship/version.rb +5 -0
- data/lib/kinship.rb +11 -0
- data/sig/kinship.rbs +4 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f7d5ced07f37ed098f40ef418d6964cbc6b6c91655365720b14150eb80435242
|
|
4
|
+
data.tar.gz: af7ec88de5e10093501c056b37e6ba1f3e5b7ed861cad54efba00a3c525682ee
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 57573a41df7c60c1c2b83bf0522a6fa21d496c6246c5039983ad6b5ae25d20f801a1eb7e229b5f1de1ffb35f4b9b319e82ded6fd5b801c6613fffd2712573f16
|
|
7
|
+
data.tar.gz: a33cde8b13290fc2b5a518b1df9160684e839ca31fb636f96fe73dafc310a30b2b0f99e30acccf8fd1541381b040691f3fc876ae301498a0b7db458ddacbdfca
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kurt Tamulonis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Kinship
|
|
2
|
+
|
|
3
|
+
**Schema‑inferred relationship graphs for Ruby & Rails**
|
|
4
|
+
|
|
5
|
+
Kinship discovers relationships between your models **automatically** by inspecting columns like `user_id`, `project_id`, etc. From that single fact, it builds a full graph of your domain — parents, children, families, paths — without `has_many`, `belongs_to`, or configuration.
|
|
6
|
+
|
|
7
|
+
It exists to solve a very real problem:
|
|
8
|
+
|
|
9
|
+
> *Rails knows your schema, but not your graph.*
|
|
10
|
+
|
|
11
|
+
Kinship makes the graph explicit.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why Kinship exists
|
|
16
|
+
|
|
17
|
+
Most Rails apps:
|
|
18
|
+
|
|
19
|
+
* Define associations manually
|
|
20
|
+
* Forget to preload
|
|
21
|
+
* Accidentally ship N+1 queries
|
|
22
|
+
* Duplicate schema knowledge in code
|
|
23
|
+
|
|
24
|
+
Example of a **very common bad query**:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
User.all.each do |user|
|
|
28
|
+
user.projects.each do |project|
|
|
29
|
+
project.tasks.each do |task|
|
|
30
|
+
task.comments.each do |comment|
|
|
31
|
+
comment.reactions.each do |reaction|
|
|
32
|
+
reaction.kind
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Rails *allows* this. It fails silently. Performance collapses.
|
|
41
|
+
|
|
42
|
+
Kinship’s philosophy:
|
|
43
|
+
|
|
44
|
+
> **If the database already encodes relationships, your application should be able to reason about them automatically.**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What Kinship gives you
|
|
49
|
+
|
|
50
|
+
From nothing more than foreign keys, Kinship builds:
|
|
51
|
+
|
|
52
|
+
* `parents(model)` – one layer up
|
|
53
|
+
* `children(model)` – one layer down
|
|
54
|
+
* `families` – connected components
|
|
55
|
+
* `path(from, to)` – shortest relationship path
|
|
56
|
+
* `to_dot` – visual graph output
|
|
57
|
+
|
|
58
|
+
No macros. No DSL. No annotations.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### Ruby / Rails
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gem install kinship
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
or in your Gemfile:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
gem "kinship"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Rails usage (recommended setup)
|
|
79
|
+
|
|
80
|
+
Create an initializer:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# config/initializers/kinship.rb
|
|
84
|
+
Rails.application.config.after_initialize do
|
|
85
|
+
Rails.application.eager_load!
|
|
86
|
+
|
|
87
|
+
KINSHIP = Kinship.build(
|
|
88
|
+
models: ApplicationRecord.descendants,
|
|
89
|
+
attribute_provider: ->(model) { model.column_names }
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
That’s it.
|
|
95
|
+
|
|
96
|
+
Kinship now understands your entire domain graph.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Example domain
|
|
101
|
+
|
|
102
|
+
Given these tables:
|
|
103
|
+
|
|
104
|
+
* `users`
|
|
105
|
+
* `projects` (`user_id`)
|
|
106
|
+
* `tasks` (`project_id`)
|
|
107
|
+
* `comments` (`task_id`)
|
|
108
|
+
* `reactions` (`comment_id`)
|
|
109
|
+
|
|
110
|
+
No associations defined.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Core API
|
|
115
|
+
|
|
116
|
+
### Parents
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
KINSHIP.parents(Project)
|
|
120
|
+
# => { user: User }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Children
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
KINSHIP.children(Project)
|
|
127
|
+
# => { tasks: Task }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Families
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
KINSHIP.families
|
|
134
|
+
# => [[User, Project, Task, Comment, Reaction]]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Path discovery
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
KINSHIP.path(User, Reaction)
|
|
141
|
+
# => [User, Project, Task, Comment, Reaction]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This is **the missing abstraction** Rails never gave you.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Graph visualization
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
puts KINSHIP.to_dot
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Output:
|
|
155
|
+
|
|
156
|
+
```dot
|
|
157
|
+
digraph Kinship {
|
|
158
|
+
"User";
|
|
159
|
+
"Project";
|
|
160
|
+
"Task";
|
|
161
|
+
"Comment";
|
|
162
|
+
"Reaction";
|
|
163
|
+
"User" -> "Project";
|
|
164
|
+
"Project" -> "Task";
|
|
165
|
+
"Task" -> "Comment";
|
|
166
|
+
"Comment" -> "Reaction";
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
You can render this with Graphviz to **see your data model**.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## What Kinship intentionally does *not* do (yet)
|
|
175
|
+
|
|
176
|
+
* Execute SQL
|
|
177
|
+
* Replace ActiveRecord
|
|
178
|
+
* Magically rewrite queries
|
|
179
|
+
|
|
180
|
+
Instead, it provides the **missing layer of understanding** required to:
|
|
181
|
+
|
|
182
|
+
* detect N+1 queries
|
|
183
|
+
* generate preload plans
|
|
184
|
+
* reason about deep filters
|
|
185
|
+
* visualize data flow
|
|
186
|
+
|
|
187
|
+
Kinship is the **map**, avoiding wrong turns.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Framework integrations
|
|
192
|
+
|
|
193
|
+
Kinship is framework‑agnostic.
|
|
194
|
+
|
|
195
|
+
It can be embedded into:
|
|
196
|
+
|
|
197
|
+
* Rails (today)
|
|
198
|
+
* Jetski (in progress)
|
|
199
|
+
* Custom ORMs
|
|
200
|
+
* Data tooling
|
|
201
|
+
|
|
202
|
+
Once embedded, *all downstream users benefit* automatically.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Roadmap (vision)
|
|
207
|
+
|
|
208
|
+
* Declarative query intent
|
|
209
|
+
* Automatic preload inference
|
|
210
|
+
* N+1 detection hooks
|
|
211
|
+
* Query planning helpers
|
|
212
|
+
* Console inspection tools
|
|
213
|
+
|
|
214
|
+
Kinship is designed to cover **the 80% of relationship problems that cause 95% of performance bugs**.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Philosophy
|
|
219
|
+
|
|
220
|
+
> Databases already encode relationships.
|
|
221
|
+
>
|
|
222
|
+
> Kinship makes them visible, navigable, and reason‑able.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
**Built by Kurt Tamulonis**
|
|
233
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module Kinship
|
|
2
|
+
class Graph
|
|
3
|
+
attr_reader :models, :parents, :children
|
|
4
|
+
|
|
5
|
+
def initialize(models:, attribute_provider:)
|
|
6
|
+
@models = models
|
|
7
|
+
@attribute_provider = attribute_provider
|
|
8
|
+
|
|
9
|
+
@parents = Hash.new { |h, k| h[k] = {} }
|
|
10
|
+
@children = Hash.new { |h, k| h[k] = {} }
|
|
11
|
+
|
|
12
|
+
build!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parents(model)
|
|
16
|
+
@parents[model] || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def children(model)
|
|
20
|
+
@children[model] || {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def families
|
|
24
|
+
visited = {}
|
|
25
|
+
groups = []
|
|
26
|
+
|
|
27
|
+
@models.each do |model|
|
|
28
|
+
next if visited[model]
|
|
29
|
+
groups << bfs(model, visited)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
groups
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def path(from, to)
|
|
36
|
+
return [from] if from == to
|
|
37
|
+
|
|
38
|
+
queue = [[from]]
|
|
39
|
+
seen = { from => true }
|
|
40
|
+
|
|
41
|
+
until queue.empty?
|
|
42
|
+
current = queue.shift
|
|
43
|
+
node = current.last
|
|
44
|
+
|
|
45
|
+
neighbors(node).each do |neighbor|
|
|
46
|
+
next if seen[neighbor]
|
|
47
|
+
|
|
48
|
+
path = current + [neighbor]
|
|
49
|
+
return path if neighbor == to
|
|
50
|
+
|
|
51
|
+
seen[neighbor] = true
|
|
52
|
+
queue << path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_dot
|
|
60
|
+
lines = []
|
|
61
|
+
lines << "digraph Kinship {"
|
|
62
|
+
|
|
63
|
+
@models.each do |model|
|
|
64
|
+
lines << " \"#{model.name}\";"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@parents.each do |child, parents|
|
|
68
|
+
parents.each_value do |parent|
|
|
69
|
+
lines << " \"#{parent.name}\" -> \"#{child.name}\";"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
lines << "}"
|
|
74
|
+
lines.join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def build!
|
|
80
|
+
lookup = @models.to_h { |m| [simple_name(m), m] }
|
|
81
|
+
|
|
82
|
+
@models.each do |child|
|
|
83
|
+
attributes_for(child).each do |attr|
|
|
84
|
+
next unless attr.end_with?("_id")
|
|
85
|
+
|
|
86
|
+
parent_key = attr.sub(/_id$/, "")
|
|
87
|
+
parent = lookup[parent_key]
|
|
88
|
+
next unless parent
|
|
89
|
+
|
|
90
|
+
@parents[child][parent_key.to_sym] = parent
|
|
91
|
+
@children[parent][pluralize(child).to_sym] = child
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def attributes_for(model)
|
|
97
|
+
attrs =
|
|
98
|
+
if @attribute_provider.respond_to?(:call)
|
|
99
|
+
@attribute_provider.call(model)
|
|
100
|
+
else
|
|
101
|
+
@attribute_provider[model]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "attribute_provider must return Array<String>" unless attrs.is_a?(Array)
|
|
105
|
+
attrs
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def neighbors(model)
|
|
109
|
+
(parents(model).values + children(model).values).uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def bfs(start, visited)
|
|
113
|
+
queue = [start]
|
|
114
|
+
group = []
|
|
115
|
+
visited[start] = true
|
|
116
|
+
|
|
117
|
+
until queue.empty?
|
|
118
|
+
node = queue.shift
|
|
119
|
+
group << node
|
|
120
|
+
|
|
121
|
+
neighbors(node).each do |n|
|
|
122
|
+
next if visited[n]
|
|
123
|
+
visited[n] = true
|
|
124
|
+
queue << n
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
group
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def simple_name(model)
|
|
132
|
+
model.name
|
|
133
|
+
.split("::")
|
|
134
|
+
.last
|
|
135
|
+
.gsub(/([a-z0-9])([A-Z])/, '\1_\2')
|
|
136
|
+
.downcase
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def pluralize(model)
|
|
140
|
+
n = simple_name(model)
|
|
141
|
+
n.end_with?("s") ? n : "#{n}s"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
data/lib/kinship.rb
ADDED
data/sig/kinship.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kinship
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- hackliteracy
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
Kinship is a schema-inferred relationship graph for Ruby applications.
|
|
15
|
+
It automatically discovers parent/child relationships between models
|
|
16
|
+
by inspecting attributes (e.g. user_id, post_id) and builds a complete
|
|
17
|
+
in-memory graph with zero configuration.
|
|
18
|
+
|
|
19
|
+
Kinship enables deep relationship traversal, automatic join planning,
|
|
20
|
+
and eliminates common N+1 query patterns without requiring has_many or
|
|
21
|
+
belongs_to declarations. It is framework-agnostic and works with Rails,
|
|
22
|
+
Jetski, and custom ORMs.
|
|
23
|
+
email:
|
|
24
|
+
- hackliteracy@gmail.com
|
|
25
|
+
executables: []
|
|
26
|
+
extensions: []
|
|
27
|
+
extra_rdoc_files: []
|
|
28
|
+
files:
|
|
29
|
+
- CHANGELOG.md
|
|
30
|
+
- LICENSE.txt
|
|
31
|
+
- README.md
|
|
32
|
+
- Rakefile
|
|
33
|
+
- lib/kinship.rb
|
|
34
|
+
- lib/kinship/graph.rb
|
|
35
|
+
- lib/kinship/version.rb
|
|
36
|
+
- sig/kinship.rbs
|
|
37
|
+
homepage: https://github.com/ktamulonis/kinship
|
|
38
|
+
licenses:
|
|
39
|
+
- MIT
|
|
40
|
+
metadata:
|
|
41
|
+
homepage_uri: https://github.com/ktamulonis/kinship
|
|
42
|
+
source_code_uri: https://github.com/ktamulonis/kinship
|
|
43
|
+
changelog_uri: https://github.com/ktamulonis/kinship/blob/main/CHANGELOG.md
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
45
|
+
post_install_message:
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.5.22
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Schema-inferred relationship graphs for Ruby applications
|
|
64
|
+
test_files: []
|