algebra_db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +64 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +8 -0
  6. data/.travis.yml +6 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +9 -0
  9. data/Gemfile.lock +63 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +195 -0
  12. data/Rakefile +6 -0
  13. data/algebra_db.gemspec +36 -0
  14. data/bin/console +91 -0
  15. data/bin/setup +8 -0
  16. data/lib/algebra_db.rb +16 -0
  17. data/lib/algebra_db/build.rb +20 -0
  18. data/lib/algebra_db/build/between.rb +25 -0
  19. data/lib/algebra_db/build/column.rb +13 -0
  20. data/lib/algebra_db/build/join.rb +35 -0
  21. data/lib/algebra_db/build/op.rb +13 -0
  22. data/lib/algebra_db/build/param.rb +9 -0
  23. data/lib/algebra_db/build/select_item.rb +22 -0
  24. data/lib/algebra_db/build/select_list.rb +64 -0
  25. data/lib/algebra_db/build/table_from.rb +10 -0
  26. data/lib/algebra_db/def.rb +7 -0
  27. data/lib/algebra_db/def/relationship.rb +20 -0
  28. data/lib/algebra_db/exec.rb +9 -0
  29. data/lib/algebra_db/exec/decoder.rb +20 -0
  30. data/lib/algebra_db/exec/delivery.rb +35 -0
  31. data/lib/algebra_db/exec/row_decoder.rb +15 -0
  32. data/lib/algebra_db/statement.rb +7 -0
  33. data/lib/algebra_db/statement/select.rb +84 -0
  34. data/lib/algebra_db/syntax_builder.rb +54 -0
  35. data/lib/algebra_db/table.rb +98 -0
  36. data/lib/algebra_db/value.rb +45 -0
  37. data/lib/algebra_db/value/array.rb +55 -0
  38. data/lib/algebra_db/value/bool.rb +31 -0
  39. data/lib/algebra_db/value/double.rb +20 -0
  40. data/lib/algebra_db/value/integer.rb +20 -0
  41. data/lib/algebra_db/value/jsonb.rb +19 -0
  42. data/lib/algebra_db/value/operations.rb +10 -0
  43. data/lib/algebra_db/value/operations/definition.rb +23 -0
  44. data/lib/algebra_db/value/operations/numeric.rb +18 -0
  45. data/lib/algebra_db/value/text.rb +9 -0
  46. data/lib/algebra_db/version.rb +3 -0
  47. metadata +146 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8b18d933ff5328a62f8a8489d851d3fcc82eb09f9ecf16dc889f9ddbda491681
4
+ data.tar.gz: b570df4ed63f8c95472e623ebc7cb99dc909e194288c69050533754e3ac0f5b4
5
+ SHA512:
6
+ metadata.gz: 67d8b4389f4e94abe3bd3914e59d39e538e746edc8832586e787cd5b987e150e5a5cc8e5c1ad51b210e7a253c711610b4dafd4388284210a8a4d3e513ca6dbac
7
+ data.tar.gz: fa491f1effcebf4ce0117efb55f8e2d0ecaf0d2f1349678078c05a97dbfe9f50c9a23079930b631e5ae6dc97dbc7f40e55864ecf10f8212f98f39704ff49e1d3
@@ -0,0 +1,64 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby: [ '2.6', '2.7', '2.5' ]
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
+ # uses: ruby/setup-ruby@v1
30
+ uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
31
+ with:
32
+ ruby-version: ${{ matrix.ruby }}
33
+ - uses: actions/cache@v2
34
+ with:
35
+ path: vendor/bundle
36
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
37
+ restore-keys: |
38
+ ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
39
+ - name: Install dependencies
40
+ run: |
41
+ bundle config path vendor/bundle
42
+ bundle install
43
+ - name: Run tests
44
+ run: COVERAGE=1 bundle exec rake
45
+ - name: Upload Coverage
46
+ uses: actions/upload-artifact@master
47
+ if: always()
48
+ with:
49
+ name: coverage-report
50
+ path: coverage
51
+ - uses: actions/cache@v2
52
+ with:
53
+ path: example/vendor/bundle
54
+ key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
55
+ restore-keys: |
56
+ ${{ runner.os }}-${{ matrix.ruby }}-example-deps-
57
+ - name: Install example dependencies for example
58
+ working-directory: example
59
+ run: |
60
+ bundle config path vendor/bundle
61
+ bundle install
62
+ - name: Run specs for example
63
+ working-directory: example
64
+ run: bundle exec rake
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ coverage/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,8 @@
1
+ Style/FrozenStringLiteralComment:
2
+ Enabled: false
3
+ AllCops:
4
+ NewCops: enable
5
+
6
+ Metrics/BlockLength:
7
+ Exclude:
8
+ - 'spec/**/*.rb'
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,3 @@
1
+ # CHANGELOG
2
+
3
+ This gem is not yet even close to any kind of release, and thus has no changelog.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in algebra_db.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 12.0'
7
+ gem 'rspec', '~> 3.0'
8
+ gem 'rspec-its'
9
+ gem 'simplecov', require: false
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ algebra_db (0.1.0)
5
+ pg (~> 1.2.3)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ byebug (11.1.3)
11
+ coderay (1.1.3)
12
+ diff-lcs (1.4.4)
13
+ docile (1.3.2)
14
+ io-console (0.5.6)
15
+ irb (1.2.3)
16
+ reline (>= 0.0.1)
17
+ method_source (1.0.0)
18
+ pg (1.2.3)
19
+ pry (0.13.1)
20
+ coderay (~> 1.1)
21
+ method_source (~> 1.0)
22
+ pry-byebug (3.9.0)
23
+ byebug (~> 11.0)
24
+ pry (~> 0.13.0)
25
+ rake (12.3.3)
26
+ reline (0.1.4)
27
+ io-console (~> 0.5)
28
+ rspec (3.9.0)
29
+ rspec-core (~> 3.9.0)
30
+ rspec-expectations (~> 3.9.0)
31
+ rspec-mocks (~> 3.9.0)
32
+ rspec-core (3.9.3)
33
+ rspec-support (~> 3.9.3)
34
+ rspec-expectations (3.9.2)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.9.0)
37
+ rspec-its (1.3.0)
38
+ rspec-core (>= 3.0.0)
39
+ rspec-expectations (>= 3.0.0)
40
+ rspec-mocks (3.9.1)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.9.0)
43
+ rspec-support (3.9.3)
44
+ simplecov (0.18.5)
45
+ docile (~> 1.1)
46
+ simplecov-html (~> 0.11)
47
+ simplecov-html (0.12.2)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ algebra_db!
54
+ irb
55
+ pry
56
+ pry-byebug
57
+ rake (~> 12.0)
58
+ rspec (~> 3.0)
59
+ rspec-its
60
+ simplecov
61
+
62
+ BUNDLED WITH
63
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Anthony Super
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.
@@ -0,0 +1,195 @@
1
+ # AlgebraDB
2
+
3
+ This is a database library for Ruby based on *relational expressions*.
4
+ Most other database libraries, like the excellent [sequel](https://github.com/jeremyevans/sequel) or [activerecord](https://github.com/rails/rails/tree/master/activerecord) are based on some idea of a *scoped dataset*, which is essentially an object that you can chain methods on to do further filtering, ordering, or what have you.
5
+ AlgebraDB is instead based on a sort of *typed query builder*.
6
+ Your queries return arrays of structs, custom-made for whatever query you're doing.
7
+ This encourages us to use the full functionality of our database, resulting in faster and more correct queries.
8
+
9
+ An example is probably illustrative...
10
+
11
+ ## Example
12
+
13
+ Let's say I have two tables.
14
+ One is a `users` table, defined like this:
15
+
16
+ ```sql
17
+ CREATE TABLE users(
18
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
19
+ first_name TEXT NOT NULL,
20
+ last_name TEXT NOT NULL
21
+ );
22
+
23
+ -- Probably some sort of "What did the user do" thing.
24
+ CREATE TABLE user_audits(
25
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIAMRY KEY,
26
+ user_id BIGINT REFERENCES users(id) NOT NULL,
27
+ scoped_granted TEXT[] NOT NULL,
28
+ values_changes JSONB DEFAULT '{}'::jsonb
29
+ );
30
+ ```
31
+
32
+ Now, these two tables are relatively nice to work with.
33
+ But, what if I wanted to run a query that gave me all the audit logs performed under similar scopes, and what users performed those?
34
+ This is already a bit of an annoying query, but it gets worse if I want to include the users' full names as well.
35
+
36
+ With AlgebraDB, we would define some classes like this:
37
+
38
+ ```ruby
39
+ ##
40
+ # Basic user table
41
+ class User < AlgebraDB::Table
42
+ self.table_name = :users
43
+
44
+ column :id, :Integer
45
+ column :first_name, :Text
46
+ column :last_name, :Text
47
+
48
+ end
49
+
50
+ ##
51
+ # Audit log for users
52
+ class UserAudit < AlgebraDB::Table
53
+ self.table_name = :user_audits
54
+
55
+ column :id, :Integer
56
+ column :user_id, :Integer
57
+ column :scopes_granted, AlgebraDB::Value::Array::Text
58
+ column :changes, AlgebraDB::Value::JSONB
59
+ end
60
+ ```
61
+
62
+ We could then run a query like this:
63
+
64
+ ```ruby
65
+ query = AlgebraDB::Statement::Select.run_syntax do
66
+ parent_audits = all(UserAudit)
67
+ parent_audit_users = joins(User) do |user|
68
+ user.id.eq(parent_audits.user_id)
69
+ end
70
+ child_audits = joins(UserAudit) do |other_audit|
71
+ other_audit.scopes_granted.overlaps(parent_audits.scopes_granted).and(
72
+ other_audits.id.neq(parent_audits.id)
73
+ )
74
+ end
75
+ child_audit_users = joins(User) do |user|
76
+ user.id.eq(child_audits.user_id)
77
+ end
78
+ select(
79
+ parent_audit_id: parent_audits.id,
80
+ parent_audit_user: parent_audit_users.first_name.append(raw_param(' ')).append(parent_audit_users.last_name),
81
+ child_audit_id: child_audits.id,
82
+ child_audit_user: child_audit_users.first_name.append(raw_param(' ')).append(child_audit_users.last_name)
83
+ )
84
+ end
85
+ ```
86
+
87
+ This, of course, is not the best code.
88
+ We're doing a lot of repetition.
89
+ However, since AlgebraDB operates on *tables* instead of *records*, we can define some cleanup easily:
90
+
91
+
92
+ ```ruby
93
+ ##
94
+ # Basic user table
95
+ class User < AlgebraDB::Table
96
+ self.table_name = :users
97
+
98
+ column :id, :Integer
99
+ column :first_name, :Text
100
+ column :last_name, :Text
101
+
102
+ ##
103
+ # Instances are a *table in a query*, so this works!
104
+ def full_name
105
+ first_name.concat(AlgebraDB::Build.param(' ')).concat(last_name)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Audit log for users
111
+ class UserAudit < AlgebraDB::Table
112
+ self.table_name = :user_audits
113
+
114
+ column :id, :Integer
115
+ column :user_id, :Integer
116
+ column :scopes_granted, AlgebraDB::Value::Array::Text
117
+ column :changes, AlgebraDB::Value::JSONB
118
+
119
+ relationship :user, User do |user|
120
+ user.id.eq(user_id)
121
+ end
122
+
123
+ relationship :similar_audits, UserAudit do |other_audit|
124
+ other_audit.scopes_granted.overlaps(scopes_granted).and(
125
+ other_audit.id.neq(id)
126
+ )
127
+ end
128
+ end
129
+ ```
130
+
131
+ Now we only need to write:
132
+
133
+ ```ruby
134
+ AlgebraDB::Statement::Select.run_syntax do
135
+ parent_audits = all(UserAudit)
136
+ parent_audit_users = join_relationship(parent_audits.user)
137
+ child_audits = join_relationship(parent_audits.similar_audits)
138
+ child_audit_users = join_relationship(child_audits.user)
139
+ select(
140
+ parent_audit_id: parent_audits.id,
141
+ parent_audit_user: parent_audit_users.full_name,
142
+ child_audit_id: child_audits.id,
143
+ child_audit_user: child_audit_users.full_name
144
+ )
145
+ end
146
+ ```
147
+
148
+ Notice a few things we did here that make this moderately magic:
149
+
150
+ 1. We joined in the same table multiple times very, very easily.
151
+ These tables have different aliases in the query, so if we wanted only child audits by a user with a name like "bob", we could have just added a `where(child_audit_users.full_name.ilike(raw_param('Bob')))`
152
+ 2. All of the user-supplied data happens via postgres parameters.
153
+ We're not putting raw SQL strings *anywhere*: even the space in the `full_name` method is parameterized!
154
+ 3. We wrote an ad-hoc aliased select list, which will be converted at runtime to ruby `Struct` instances with the right keys.
155
+ That means we can sort those records, use them as keys in a hash, or do whatever we want, easily.
156
+
157
+ In the future this will make way for powerful aggregation functionality.
158
+ Postgres already has `ARRAY_AGG` and `JSONB_AGG`.
159
+ Instead of making multiple queries to do eager-loading, or making one big query and then performing reassociation yourself, you'll be able to let the database (which is a hell of a lot faster than Ruby) do it for you!
160
+
161
+
162
+ ## Installation
163
+
164
+ Add this line to your application's Gemfile:
165
+
166
+ ```ruby
167
+ gem 'algebra_db'
168
+ ```
169
+
170
+ And then execute:
171
+
172
+ $ bundle install
173
+
174
+ Or install it yourself as:
175
+
176
+ $ gem install algebra_db
177
+
178
+ ## Usage
179
+
180
+ TODO: Write usage instructions here
181
+
182
+ ## Development
183
+
184
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
185
+
186
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
187
+
188
+ ## Contributing
189
+
190
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/algebra_db.
191
+
192
+
193
+ ## License
194
+
195
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,36 @@
1
+ require_relative 'lib/algebra_db/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'algebra_db'
5
+ spec.version = AlgebraDB::VERSION
6
+ spec.authors = ['Anthony Super']
7
+ spec.email = ['anthony@noided.media']
8
+
9
+ spec.summary = 'Use algebra to talk to your DB'
10
+ spec.description = 'A typed query builder for ruby, basically'
11
+ spec.homepage = 'https://github.com/AnthonySuper/algebra_db'
12
+ spec.license = 'MIT'
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
14
+
15
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = 'https://github.com/AnthonySuper/algebra_db/CHANGELOG.md'
19
+ # spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
20
+ # spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'pg', '~> 1.2.3'
32
+
33
+ spec.add_development_dependency 'irb'
34
+ spec.add_development_dependency 'pry'
35
+ spec.add_development_dependency 'pry-byebug'
36
+ end