metka 2.3.3 → 3.0.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint_code.yml +9 -6
  3. data/.github/workflows/lint_docs.yml +15 -16
  4. data/.github/workflows/release.yml +23 -0
  5. data/.github/workflows/tests.yml +97 -0
  6. data/.gitignore +16 -7
  7. data/.rubocop.yml +8 -16
  8. data/.ruby-version +1 -1
  9. data/Gemfile +1 -1
  10. data/README.md +273 -236
  11. data/Rakefile +9 -4
  12. data/assets/metka-icon.svg +23 -0
  13. data/assets/metka-logo.svg +28 -0
  14. data/benchmark/Gemfile +17 -0
  15. data/benchmark/README.md +170 -0
  16. data/benchmark/benchmark.rb +718 -0
  17. data/benchmark/results.sqlite.txt +131 -0
  18. data/benchmark/results.txt +150 -0
  19. data/bin/setup +7 -0
  20. data/docs/superpowers/plans/2026-08-18-cloud-table-naming.md +316 -0
  21. data/docs/superpowers/specs/2026-08-18-cloud-table-naming-design.md +105 -0
  22. data/forspell.dict +3 -1
  23. data/gemfiles/rails71.gemfile +6 -0
  24. data/gemfiles/rails72.gemfile +6 -0
  25. data/gemfiles/rails80.gemfile +6 -0
  26. data/gemfiles/rails81.gemfile +6 -0
  27. data/gemfiles/rubocop.gemfile +2 -2
  28. data/lib/generators/metka/strategies/index/index_generator.rb +77 -0
  29. data/lib/generators/metka/strategies/index/templates/migration.rb.erb +89 -0
  30. data/lib/generators/metka/strategies/table/table_generator.rb +83 -0
  31. data/lib/generators/metka/strategies/table/templates/migration.rb.erb +128 -0
  32. data/lib/generators/metka/strategies/table/templates/migration.sqlite.rb.erb +93 -0
  33. data/lib/metka/generic_parser.rb +28 -14
  34. data/lib/metka/model.rb +121 -51
  35. data/lib/metka/query_builder.rb +45 -49
  36. data/lib/metka/tag_list.rb +16 -8
  37. data/lib/metka/tags_query.rb +93 -0
  38. data/lib/metka/version.rb +1 -1
  39. data/lib/metka.rb +24 -10
  40. data/metka.gemspec +21 -16
  41. metadata +45 -92
  42. data/.github/workflows/specs.yml +0 -86
  43. data/.rspec +0 -2
  44. data/Gemfile.lock +0 -201
  45. data/gemfiles/rails52.gemfile +0 -6
  46. data/gemfiles/rails6.gemfile +0 -6
  47. data/gemfiles/rails61.gemfile +0 -6
  48. data/lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb +0 -73
  49. data/lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb +0 -54
  50. data/lib/generators/metka/strategies/view/templates/migration.rb.erb +0 -26
  51. data/lib/generators/metka/strategies/view/view_generator.rb +0 -70
  52. data/lib/metka/query_builder/all_tags_query.rb +0 -11
  53. data/lib/metka/query_builder/any_tags_query.rb +0 -11
  54. data/lib/metka/query_builder/base_query.rb +0 -48
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %>
4
- def up
5
- execute <<-SQL
6
- CREATE OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view() RETURNS trigger LANGUAGE plpgsql AS $$
7
- BEGIN
8
- IF TG_OP = 'INSERT' AND
9
- (<%= source_columns.map { |column| "NEW.#{column} IS NOT NULL" }.join(' OR ') %>) THEN
10
- REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
11
- END IF;
12
-
13
- IF TG_OP = 'UPDATE' AND
14
- (<%= source_columns.map { |column| "OLD.#{column} IS DISTINCT FROM NEW.#{column}" }.join(' OR ') %>) THEN
15
- REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
16
- END IF;
17
-
18
- IF TG_OP = 'DELETE' AND
19
- (<%= source_columns.map { |column| "OLD.#{column} IS NOT NULL" }.join(' OR ') %>) THEN
20
- REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
21
- END IF;
22
- RETURN NEW;
23
- END $$;
24
-
25
- DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
26
- CREATE MATERIALIZED VIEW <%= view_name %> AS
27
- SELECT
28
- tag_name,
29
- COUNT(*) AS taggings_count
30
- FROM (
31
- SELECT UNNEST
32
- (<%= source_columns.join(' || ') %>) AS tag_name
33
- FROM
34
- <%= source_table_name %>
35
- ) subquery
36
- GROUP BY
37
- tag_name;
38
-
39
- CREATE UNIQUE INDEX idx_<%= source_table_name %>_<%= source_columns_names %> ON <%= view_name %>(tag_name);
40
-
41
- CREATE TRIGGER metka_on_<%= source_table_name %>_<%= source_columns_names %>
42
- AFTER UPDATE OR INSERT OR DELETE ON <%= source_table_name %> FOR EACH ROW
43
- EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
44
- SQL
45
- end
46
-
47
- def down
48
- execute <<-SQL
49
- DROP TRIGGER IF EXISTS metka_on_<%= source_table_name %>_<%= source_columns_names %> ON <%= source_table_name %>;
50
- DROP FUNCTION IF EXISTS metka_refresh_<%= view_name %>_materialized_view;
51
- DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
52
- SQL
53
- end
54
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %>
4
- def up
5
- execute <<-SQL
6
- CREATE OR REPLACE VIEW <%= view_name %> AS
7
- SELECT
8
- tag_name,
9
- COUNT(*) AS taggings_count
10
- FROM (
11
- SELECT UNNEST
12
- (<%= source_columns.join(' || ') %>) AS tag_name
13
- FROM
14
- <%= source_table_name %>
15
- ) subquery
16
- GROUP BY
17
- tag_name;
18
- SQL
19
- end
20
-
21
- def down
22
- execute <<-SQL
23
- DROP VIEW <%= view_name %>;
24
- SQL
25
- end
26
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rails/generators'
4
- require 'rails/generators/active_record'
5
-
6
- module Metka
7
- module Generators
8
- module Strategies
9
- class ViewGenerator < ::Rails::Generators::Base # :nodoc:
10
- include Rails::Generators::Migration
11
-
12
- DEFAULT_SOURCE_COLUMNS = ['tags'].freeze
13
-
14
- desc <<~LONGDESC
15
- Generates migration to implement view strategy for Metka
16
-
17
- > $ rails g metka:strategies:view --source-table-name=NAME_OF_TABLE_WITH_TAGS
18
- LONGDESC
19
-
20
- source_root File.expand_path('templates', __dir__)
21
-
22
- class_option :source_table_name, type: :string, required: true,
23
- desc: 'Name of the table that has a column with tags'
24
-
25
- class_option :source_columns, type: :array, default: DEFAULT_SOURCE_COLUMNS,
26
- desc: 'List of the tagged columns names'
27
-
28
- class_option :view_name, type: :string,
29
- desc: 'Custom name for the resulting view'
30
-
31
- def generate_migration
32
- migration_template 'migration.rb.erb', "db/migrate/#{migration_name}.rb"
33
- end
34
-
35
- no_tasks do
36
- def source_table_name
37
- options[:source_table_name]
38
- end
39
-
40
- def source_columns
41
- options[:source_columns]
42
- end
43
-
44
- def source_columns_names
45
- source_columns.join('_and_')
46
- end
47
-
48
- def view_name
49
- return options[:view_name] if options[:view_name]
50
-
51
- columns_sequence = source_columns == DEFAULT_SOURCE_COLUMNS ? nil : "_with_#{source_columns_names}"
52
- "tagged#{columns_sequence}_#{source_table_name}"
53
- end
54
-
55
- def migration_name
56
- "create_#{view_name}_view"
57
- end
58
-
59
- def migration_class_name
60
- migration_name.classify
61
- end
62
- end
63
-
64
- def self.next_migration_number(dir)
65
- ::ActiveRecord::Generators::Base.next_migration_number(dir)
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Metka
4
- class AllTagsQuery < BaseQuery
5
- private
6
-
7
- def infix_operator
8
- '@>'
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Metka
4
- class AnyTagsQuery < BaseQuery
5
- private
6
-
7
- def infix_operator
8
- '&&'
9
- end
10
- end
11
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'singleton'
4
-
5
- module Metka
6
- class BaseQuery
7
- include Singleton
8
-
9
- def call(model, column_name, tag_list)
10
- tags = tag_list.to_a
11
-
12
- if tags.one?
13
- value = Arel::Nodes::SqlLiteral.new(
14
- ActiveRecord::Base.sanitize_sql_for_conditions(['?', tags.first])
15
- )
16
-
17
- column_cast = Arel::Nodes::NamedFunction.new(
18
- 'ANY',
19
- [model.arel_table[column_name]]
20
- )
21
-
22
- Arel::Nodes::Equality.new(value, column_cast)
23
- else
24
- value = Arel::Nodes::SqlLiteral.new(
25
- ActiveRecord::Base.sanitize_sql_for_conditions(['ARRAY[?]::varchar[]', tags])
26
- )
27
-
28
- Arel::Nodes::InfixOperation.new(infix_operator, model.arel_table[column_name], value)
29
- end
30
- # column_cast = Arel::Nodes::NamedFunction.new(
31
- # 'CAST',
32
- # [model.arel_table[column_name].as('text[]')]
33
- # )
34
-
35
- # value = Arel::Nodes::SqlLiteral.new(
36
- # ActiveRecord::Base.sanitize_sql_for_conditions(['ARRAY[?]::varchar[]', tag_list.to_a])
37
- # )
38
-
39
- # value_cast = Arel::Nodes::NamedFunction.new(
40
- # 'CAST',
41
- # [value.as('text[]')]
42
- # )
43
-
44
- # # Arel::Nodes::InfixOperation.new(infix_operator, column_cast, value_cast)
45
- # Arel::Nodes::InfixOperation.new(infix_operator, model.arel_table[column_name], value)
46
- end
47
- end
48
- end