scenic 1.4.0 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +78 -0
  3. data/.gitignore +1 -0
  4. data/.hound.yml +2 -4
  5. data/.rubocop.yml +129 -0
  6. data/{NEWS.md → CHANGELOG.md} +65 -13
  7. data/CODE_OF_CONDUCT.md +76 -0
  8. data/CONTRIBUTING.md +7 -9
  9. data/Gemfile +13 -1
  10. data/LICENSE.txt +1 -1
  11. data/README.md +33 -25
  12. data/Rakefile +2 -2
  13. data/SECURITY.md +14 -0
  14. data/bin/setup +9 -4
  15. data/lib/generators/scenic/materializable.rb +9 -0
  16. data/lib/generators/scenic/model/model_generator.rb +2 -2
  17. data/lib/generators/scenic/view/USAGE +1 -0
  18. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  19. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
  20. data/lib/generators/scenic/view/view_generator.rb +19 -9
  21. data/lib/scenic/adapters/postgres.rb +20 -7
  22. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
  23. data/lib/scenic/adapters/postgres/views.rb +10 -1
  24. data/lib/scenic/command_recorder.rb +2 -1
  25. data/lib/scenic/command_recorder/statement_arguments.rb +9 -1
  26. data/lib/scenic/configuration.rb +1 -1
  27. data/lib/scenic/definition.rb +1 -1
  28. data/lib/scenic/schema_dumper.rb +2 -2
  29. data/lib/scenic/statements.rb +24 -6
  30. data/lib/scenic/version.rb +1 -1
  31. data/lib/scenic/view.rb +1 -2
  32. data/scenic.gemspec +21 -23
  33. data/spec/acceptance/user_manages_views_spec.rb +2 -1
  34. data/spec/dummy/app/models/application_record.rb +5 -0
  35. data/spec/dummy/config/database.yml +5 -0
  36. data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
  37. data/spec/generators/scenic/view/view_generator_spec.rb +10 -4
  38. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
  39. data/spec/scenic/adapters/postgres_spec.rb +23 -3
  40. data/spec/scenic/command_recorder_spec.rb +15 -1
  41. data/spec/scenic/definition_spec.rb +7 -1
  42. data/spec/scenic/schema_dumper_spec.rb +17 -2
  43. data/spec/scenic/statements_spec.rb +48 -13
  44. data/spec/spec_helper.rb +1 -1
  45. data/spec/support/generator_spec_setup.rb +1 -1
  46. metadata +22 -41
  47. data/.travis.yml +0 -44
  48. data/Appraisals +0 -33
  49. data/bin/appraisal +0 -16
  50. data/gemfiles/rails40.gemfile +0 -8
  51. data/gemfiles/rails41.gemfile +0 -8
  52. data/gemfiles/rails42.gemfile +0 -8
  53. data/gemfiles/rails42_with_fg_rails.gemfile +0 -10
  54. data/gemfiles/rails50.gemfile +0 -8
  55. data/gemfiles/rails51.gemfile +0 -8
  56. data/gemfiles/rails_edge.gemfile +0 -8
@@ -1,24 +1,22 @@
1
1
  # Contributing
2
2
 
3
3
  We love contributions from everyone. By participating in this project, you
4
- agree to abide by the thoughtbot [code of conduct].
4
+ agree to abide by our [code of conduct].
5
5
 
6
- [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
7
-
8
- We expect everyone to follow the code of conduct anywhere in thoughtbot's
9
- project codebases, issue trackers, chatrooms, and mailing lists.
6
+ [code of conduct]: CODE_OF_CONDUCT.md
10
7
 
11
8
  ## Contributing Code
12
9
 
13
10
  1. Fork the repository.
14
11
  2. Run `bin/setup`, which will install dependencies and create the dummy
15
12
  application database.
16
- 3. Run `bin/appraisal rake` to verify that the tests pass against all
17
- supported versions of Rails.
18
- 4. Make your change with new passing tests, following the [style guide].
13
+ 3. Run `rake` to verify that the tests pass against the version of Rails you are
14
+ running locally.
15
+ 4. Make your change with new passing tests, following existing style.
19
16
  5. Write a [good commit message], push your fork, and submit a pull request.
17
+ 6. CI will run the test suite on all configured versions of Ruby and Rails.
18
+ Address any failures.
20
19
 
21
- [style guide]: https://github.com/thoughtbot/guides/tree/master/style
22
20
  [good commit message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
23
21
 
24
22
  Others will give constructive feedback. This is a time for discussion and
data/Gemfile CHANGED
@@ -1,4 +1,16 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in scenic.gemspec
4
4
  gemspec
5
+
6
+ rails_version = ENV.fetch("RAILS_VERSION", "6.0")
7
+
8
+ if rails_version == "master"
9
+ rails_constraint = { github: "rails/rails" }
10
+ else
11
+ rails_constraint = "~> #{rails_version}.0"
12
+ end
13
+
14
+ gem "rails", rails_constraint
15
+ gem "sprockets", "< 4.0.0"
16
+ gem "pg", "~> 1.1"
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2016 Derek Prior, Caleb Thompson, and thoughtbot.
1
+ Copyright (c) 2014-2020 Derek Prior, Caleb Hearth, and thoughtbot.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Scenic
2
2
 
3
- ![Scenic Landscape](https://images.thoughtbot.com/announcing-scenic--versioned-database-views-for-rails/MRUcPsxrTGCeWKyE59Zg_landscape.png)
3
+ ![Scenic Landscape](https://user-images.githubusercontent.com/152152/49344534-a8817480-f646-11e8-8431-3d95d349c070.png)
4
4
 
5
- [![Build Status](https://travis-ci.org/thoughtbot/scenic.svg)](https://travis-ci.org/thoughtbot/scenic)
6
- [![Code Climate](https://codeclimate.com/repos/53c9736269568066a3000c35/badges/85aa9b19f3037252c55d/gpa.svg)](https://codeclimate.com/repos/53c9736269568066a3000c35/feed)
7
- [![Documentation Quality](http://inch-ci.org/github/thoughtbot/scenic.svg?branch=master)](http://inch-ci.org/github/thoughtbot/scenic)
5
+ [![Build Status](https://github.com/scenic-views/scenic/workflows/CI/badge.svg)](https://github.com/scenic-views/scenic/actions?query=workflow%3ACI+branch%3Amaster)
6
+ [![Documentation Quality](http://inch-ci.org/github/scenic-views/scenic.svg?branch=master)](http://inch-ci.org/github/scenic-views/scenic)
7
+ [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
8
8
 
9
9
  Scenic adds methods to `ActiveRecord::Migration` to create and manage database
10
10
  views in Rails.
@@ -21,6 +21,12 @@ Scenic ships with support for PostgreSQL. The adapter is configurable (see
21
21
  `Scenic::Configuration`) and has a minimal interface (see
22
22
  `Scenic::Adapters::Postgres`) that other gems can provide.
23
23
 
24
+ ## So how do I install this?
25
+
26
+ If you're using Postgres, Add `gem "scenic"` to your Gemfile and run `bundle
27
+ install`. If you're using something other than Postgres, check out the available
28
+ [third party adapters](https://github.com/scenic-views/scenic#faqs).
29
+
24
30
  ## Great, how do I create a view?
25
31
 
26
32
  You've got this great idea for a view you'd like to call `search_results`. You
@@ -88,7 +94,7 @@ when some of those views may be materialized and take a long time to recreate.
88
94
 
89
95
  You can use `replace_view` to generate a CREATE OR REPLACE VIEW SQL statement.
90
96
 
91
- See postgresql documentation on how this works:
97
+ See Postgres documentation on how this works:
92
98
  http://www.postgresql.org/docs/current/static/sql-createview.html
93
99
 
94
100
  To start replacing a view run the generator like for a regular change:
@@ -129,11 +135,9 @@ ActiveRecord or ARel queries. As far as ActiveRecord is concerned, a view is
129
135
  no different than a table.
130
136
 
131
137
  ```ruby
132
- class SearchResult < ActiveRecord::Base
138
+ class SearchResult < ApplicationRecord
133
139
  belongs_to :searchable, polymorphic: true
134
140
 
135
- private
136
-
137
141
  # this isn't strictly necessary, but it will prevent
138
142
  # rails from calling save, which would fail anyway.
139
143
  def readonly?
@@ -198,6 +202,7 @@ Scenic gives you `drop_view` too:
198
202
  ```ruby
199
203
  def change
200
204
  drop_view :search_results, revert_to_version: 2
205
+ drop_view :materialized_admin_reports, revert_to_version: 3, materialized: true
201
206
  end
202
207
  ```
203
208
 
@@ -213,7 +218,7 @@ You can get around these issues by setting the primary key column on your Rails
213
218
  model like so:
214
219
 
215
220
  ```ruby
216
- class People < ActiveRecord::Base
221
+ class People < ApplicationRecord
217
222
  self.primary_key = :my_unique_identifier_field
218
223
  end
219
224
  ```
@@ -231,31 +236,34 @@ add_column :posts, :title, :string
231
236
  update_view :posts_with_aggregate_data, version: 2, revert_to_version: 2
232
237
  ```
233
238
 
234
- **When will you support MySQL?**
239
+ **When will you support MySQL, SQLite, or other databases?**
235
240
 
236
- We have no plans to add first-party support for MySQL at this time because we
237
- (the maintainers) do not currently have a use for it. It's our experience that
238
- maintaining a library effectively requires regular use of its features. We're
239
- not in a good position to support MySQL users.
241
+ We have no plans to add first-party adapters for other relational databases at
242
+ this time because we (the maintainers) do not currently have a use for them.
243
+ It's our experience that maintaining a library effectively requires regular use
244
+ of its features. We're not in a good position to support MySQL, SQLite or other
245
+ database users.
240
246
 
241
247
  Scenic *does* support configuring different database adapters and should be
242
248
  extendable with adapter libraries. If you implement such an adapter, we're happy
243
249
  to review and link to it. We're also happy to make changes that would better
244
250
  accommodate adapter gems.
245
251
 
246
- ## About
252
+ We are aware of the following existing adapter libraries for Scenic which may
253
+ meet your needs:
247
254
 
248
- Scenic is maintained by [Derek Prior] and [Caleb Thompson], funded by
249
- thoughtbot, inc. The names and logos for thoughtbot are trademarks of
250
- thoughtbot, inc.
255
+ * [scenic_sqlite_adapter](https://github.com/pdebelak/scenic_sqlite_adapter)
256
+ * [scenic-mysql_adapter](https://github.com/EmpaticoOrg/scenic-mysql_adapter)
257
+ * [scenic-sqlserver-adapter](https://github.com/ClickMechanic/scenic_sqlserver_adapter)
258
+ * [scenic-oracle_adapter](https://github.com/cdinger/scenic-oracle_adapter)
251
259
 
252
- [Derek Prior]: http://prioritized.net
253
- [Caleb Thompson]: http://calebthompson.io
260
+ Please note that the maintainers of Scenic make no assertions about the
261
+ quality or security of the above adapters.
254
262
 
255
- ![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg)
263
+ ## About
256
264
 
257
- We love open source software! See [our other projects][community] or [hire
258
- us][hire] to help build your product.
265
+ Scenic is maintained by [Derek Prior], [Caleb Hearth], and you, our
266
+ contributors.
259
267
 
260
- [community]: https://thoughtbot.com/community?utm_source=github
261
- [hire]: https://thoughtbot.com/hire-us?utm_source=github
268
+ [Derek Prior]: http://prioritized.net
269
+ [Caleb Hearth]: http://calebhearth.com
data/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
@@ -0,0 +1,14 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Scenic maintainenance is a volunteer effort. We will do our best to fix
6
+ forward but do not offer backported fixes. As such, the only "supported" version of Scenic is whichever was most recently released.
7
+
8
+ ## Reporting a Vulnerability
9
+
10
+ Please report any discovered security vulnerabilities to Scenic's primary
11
+ volunteer maintainers, derekprior@gmail.com and caleb@calebhearth.com.
12
+
13
+ We will respond as soon as possible with any follow-up questions or details
14
+ on how we plan to handle the issue.
data/bin/setup CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  set -e
4
4
 
5
+ # CI-specific setup
6
+ if [ -n "$GITHUB_ACTIONS" ]; then
7
+ bundle config path vendor/bundle
8
+ bundle config jobs 4
9
+ bundle config retry 3
10
+ git config --global user.name 'GitHub Actions'
11
+ git config --global user.email 'github-actions@example.com'
12
+ fi
13
+
5
14
  gem install bundler --conservative
6
15
  bundle check || bundle install
7
16
 
8
- if [ -z "$CI" ]; then
9
- bundle exec appraisal install
10
- fi
11
-
12
17
  bundle exec rake dummy:db:drop
13
18
  bundle exec rake dummy:db:create
@@ -10,6 +10,11 @@ module Scenic
10
10
  required: false,
11
11
  desc: "Makes the view materialized",
12
12
  default: false
13
+ class_option :no_data,
14
+ type: :boolean,
15
+ required: false,
16
+ desc: "Adds WITH NO DATA when materialized view creates/updates",
17
+ default: false
13
18
  end
14
19
 
15
20
  private
@@ -17,6 +22,10 @@ module Scenic
17
22
  def materialized?
18
23
  options[:materialized]
19
24
  end
25
+
26
+ def no_data?
27
+ options[:no_data]
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -8,7 +8,7 @@ module Scenic
8
8
  # @api private
9
9
  class ModelGenerator < Rails::Generators::NamedBase
10
10
  include Scenic::Generators::Materializable
11
- source_root File.expand_path("../templates", __FILE__)
11
+ source_root File.expand_path("templates", __dir__)
12
12
 
13
13
  def invoke_rails_model_generator
14
14
  invoke "model",
@@ -35,7 +35,7 @@ module Scenic
35
35
 
36
36
  def evaluate_template(source)
37
37
  source = File.expand_path(find_in_source_paths(source.to_s))
38
- context = instance_eval("binding")
38
+ context = instance_eval("binding", __FILE__, __LINE__)
39
39
  ERB.new(
40
40
  ::File.binread(source),
41
41
  nil,
@@ -6,6 +6,7 @@ Description:
6
6
  and a migration to replace the old version with the new.
7
7
 
8
8
  To create a materialized view, pass the '--materialized' option.
9
+ To create a materialized view with NO DATA, pass '--no-data' option.
9
10
 
10
11
  Examples:
11
12
  rails generate scenic:view searches
@@ -1,5 +1,5 @@
1
1
  class <%= migration_class_name %> < <%= activerecord_migration_class %>
2
2
  def change
3
- create_view <%= formatted_plural_name %><%= ", materialized: true" if materialized? %>
3
+ create_view <%= formatted_plural_name %><%= create_view_options %>
4
4
  end
5
5
  end
@@ -4,7 +4,7 @@ class <%= migration_class_name %> < <%= activerecord_migration_class %>
4
4
  update_view <%= formatted_plural_name %>,
5
5
  version: <%= version %>,
6
6
  revert_to_version: <%= previous_version %>,
7
- materialized: true
7
+ materialized: <%= no_data? ? "{ no_data: true }" : true %>
8
8
  <%- else -%>
9
9
  update_view <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
10
10
  <%- end -%>
@@ -8,7 +8,7 @@ module Scenic
8
8
  class ViewGenerator < Rails::Generators::NamedBase
9
9
  include Rails::Generators::Migration
10
10
  include Scenic::Generators::Materializable
11
- source_root File.expand_path("../templates", __FILE__)
11
+ source_root File.expand_path("templates", __dir__)
12
12
 
13
13
  def create_views_directory
14
14
  unless views_directory_path.exist?
@@ -56,7 +56,7 @@ module Scenic
56
56
 
57
57
  def migration_class_name
58
58
  if creating_new_view?
59
- "Create#{class_name.gsub('.', '').pluralize}"
59
+ "Create#{class_name.tr('.', '').pluralize}"
60
60
  else
61
61
  "Update#{class_name.pluralize}ToVersion#{version}"
62
62
  end
@@ -64,7 +64,7 @@ module Scenic
64
64
 
65
65
  def activerecord_migration_class
66
66
  if ActiveRecord::Migration.respond_to?(:current_version)
67
- "ActiveRecord::Migration[5.0]"
67
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
68
68
  else
69
69
  "ActiveRecord::Migration"
70
70
  end
@@ -73,8 +73,14 @@ module Scenic
73
73
 
74
74
  private
75
75
 
76
+ alias singular_name file_name
77
+
78
+ def file_name
79
+ super.tr(".", "_")
80
+ end
81
+
76
82
  def views_directory_path
77
- @views_directory_path ||= Rails.root.join(*%w(db views))
83
+ @views_directory_path ||= Rails.root.join("db", "views")
78
84
  end
79
85
 
80
86
  def version_regex
@@ -82,7 +88,7 @@ module Scenic
82
88
  end
83
89
 
84
90
  def creating_new_view?
85
- previous_version == 0
91
+ previous_version.zero?
86
92
  end
87
93
 
88
94
  def definition
@@ -93,10 +99,6 @@ module Scenic
93
99
  Scenic::Definition.new(plural_file_name, previous_version)
94
100
  end
95
101
 
96
- def plural_file_name
97
- @plural_file_name ||= file_name.pluralize.gsub(".", "_")
98
- end
99
-
100
102
  def destroying?
101
103
  behavior == :revoke
102
104
  end
@@ -109,6 +111,14 @@ module Scenic
109
111
  end
110
112
  end
111
113
 
114
+ def create_view_options
115
+ if materialized?
116
+ ", materialized: #{no_data? ? '{ no_data: true }' : true}"
117
+ else
118
+ ""
119
+ end
120
+ end
121
+
112
122
  def destroying_initial_view?
113
123
  destroying? && version == 1
114
124
  end
@@ -33,7 +33,7 @@ module Scenic
33
33
  #
34
34
  # @example
35
35
  # Scenic.configure do |config|
36
- # config.adapter = Scenic::Adapters::Postgres.new
36
+ # config.database = Scenic::Adapters::Postgres.new
37
37
  # end
38
38
  def initialize(connectable = ActiveRecord::Base)
39
39
  @connectable = connectable
@@ -122,6 +122,9 @@ module Scenic
122
122
  #
123
123
  # @param name The name of the materialized view to create
124
124
  # @param sql_definition The SQL schema that defines the materialized view.
125
+ # @param no_data [Boolean] Default: false. Set to true to create
126
+ # materialized view without running the associated query. You will need
127
+ # to perform a non-concurrent refresh to populate with data.
125
128
  #
126
129
  # This is typically called in a migration via {Statements#create_view}.
127
130
  #
@@ -129,9 +132,14 @@ module Scenic
129
132
  # in use does not support materialized views.
130
133
  #
131
134
  # @return [void]
132
- def create_materialized_view(name, sql_definition)
135
+ def create_materialized_view(name, sql_definition, no_data: false)
133
136
  raise_unless_materialized_views_supported
134
- execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
137
+
138
+ execute <<-SQL
139
+ CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS
140
+ #{sql_definition.rstrip.chomp(';')}
141
+ #{'WITH NO DATA' if no_data};
142
+ SQL
135
143
  end
136
144
 
137
145
  # Updates a materialized view in the database.
@@ -144,17 +152,20 @@ module Scenic
144
152
  #
145
153
  # @param name The name of the view to update
146
154
  # @param sql_definition The SQL schema for the updated view.
155
+ # @param no_data [Boolean] Default: false. Set to true to create
156
+ # materialized view without running the associated query. You will need
157
+ # to perform a non-concurrent refresh to populate with data.
147
158
  #
148
159
  # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
149
160
  # in use does not support materialized views.
150
161
  #
151
162
  # @return [void]
152
- def update_materialized_view(name, sql_definition)
163
+ def update_materialized_view(name, sql_definition, no_data: false)
153
164
  raise_unless_materialized_views_supported
154
165
 
155
166
  IndexReapplication.new(connection: connection).on(name) do
156
167
  drop_materialized_view(name)
157
- create_materialized_view(name, sql_definition)
168
+ create_materialized_view(name, sql_definition, no_data: no_data)
158
169
  end
159
170
  end
160
171
 
@@ -198,8 +209,9 @@ module Scenic
198
209
  # @return [void]
199
210
  def refresh_materialized_view(name, concurrently: false, cascade: false)
200
211
  raise_unless_materialized_views_supported
212
+
201
213
  if cascade
202
- refresh_dependencies_for(name)
214
+ refresh_dependencies_for(name, concurrently: concurrently)
203
215
  end
204
216
 
205
217
  if concurrently
@@ -231,11 +243,12 @@ module Scenic
231
243
  end
232
244
  end
233
245
 
234
- def refresh_dependencies_for(name)
246
+ def refresh_dependencies_for(name, concurrently: false)
235
247
  Scenic::Adapters::Postgres::RefreshDependencies.call(
236
248
  name,
237
249
  self,
238
250
  connection,
251
+ concurrently: concurrently,
239
252
  )
240
253
  end
241
254
  end
@@ -2,25 +2,29 @@ module Scenic
2
2
  module Adapters
3
3
  class Postgres
4
4
  class RefreshDependencies
5
- def self.call(name, adapter, connection)
6
- new(name, adapter, connection).call
5
+ def self.call(name, adapter, connection, concurrently: false)
6
+ new(name, adapter, connection, concurrently: concurrently).call
7
7
  end
8
8
 
9
- def initialize(name, adapter, connection)
9
+ def initialize(name, adapter, connection, concurrently:)
10
10
  @name = name
11
11
  @adapter = adapter
12
12
  @connection = connection
13
+ @concurrently = concurrently
13
14
  end
14
15
 
15
16
  def call
16
17
  dependencies.each do |dependency|
17
- adapter.refresh_materialized_view(dependency)
18
+ adapter.refresh_materialized_view(
19
+ dependency,
20
+ concurrently: concurrently,
21
+ )
18
22
  end
19
23
  end
20
24
 
21
25
  private
22
26
 
23
- attr_reader :name, :adapter, :connection
27
+ attr_reader :name, :adapter, :connection, :concurrently
24
28
 
25
29
  class DependencyParser
26
30
  def initialize(raw_dependencies, view_to_refresh)
@@ -46,10 +50,20 @@ module Scenic
46
50
  def to_sorted_array
47
51
  dependency_hash = parse_to_hash(raw_dependencies)
48
52
  sorted_arr = tsort(dependency_hash)
53
+
49
54
  idx = sorted_arr.find_index do |dep|
50
- dep.include?(view_to_refresh.to_s)
55
+ if view_to_refresh.to_s.include?(".")
56
+ dep == view_to_refresh.to_s
57
+ else
58
+ dep.ends_with?(".#{view_to_refresh}")
59
+ end
60
+ end
61
+
62
+ if idx.present?
63
+ sorted_arr[0...idx]
64
+ else
65
+ []
51
66
  end
52
- sorted_arr[0...idx]
53
67
  end
54
68
 
55
69
  private