code0-zero_track 0.0.0 → 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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +0 -0
  3. data/README.md +48 -0
  4. data/Rakefile +0 -0
  5. data/lib/code0/zero_track/context.rb +133 -0
  6. data/lib/code0/zero_track/database/column_methods.rb +35 -0
  7. data/lib/code0/zero_track/database/migration.rb +26 -0
  8. data/lib/code0/zero_track/database/migration_helpers/add_column_enhancements.rb +41 -0
  9. data/lib/code0/zero_track/database/migration_helpers/constraint_helpers.rb +22 -0
  10. data/lib/code0/zero_track/database/migration_helpers/index_helpers.rb +18 -0
  11. data/lib/code0/zero_track/database/migration_helpers/table_enhancements.rb +78 -0
  12. data/lib/code0/zero_track/database/postgresql_adapter/dump_schema_versions_mixin.rb +27 -0
  13. data/lib/code0/zero_track/database/postgresql_database_tasks/load_schema_versions_mixin.rb +26 -0
  14. data/lib/code0/zero_track/database/schema_cleaner.rb +51 -0
  15. data/lib/code0/zero_track/database/schema_migrations/context.rb +48 -0
  16. data/lib/code0/zero_track/database/schema_migrations/migrations.rb +62 -0
  17. data/lib/code0/zero_track/database/schema_migrations.rb +32 -0
  18. data/lib/code0/zero_track/injectors/active_record_schema_migrations.rb +20 -0
  19. data/lib/code0/zero_track/injectors/active_record_timestamps.rb +21 -0
  20. data/lib/code0/zero_track/loggable.rb +48 -0
  21. data/lib/code0/zero_track/logs/json_formatter.rb +42 -0
  22. data/lib/code0/zero_track/memoize.rb +50 -0
  23. data/lib/code0/zero_track/railtie.rb +15 -0
  24. data/lib/code0/zero_track/version.rb +1 -1
  25. data/lib/code0/zero_track.rb +10 -2
  26. data/lib/tasks/code0/zero_track_tasks.rake +33 -4
  27. metadata +53 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f6505ffccae6a321db55632618e6a624580871f44cc65aba902e4b59094d4c
4
- data.tar.gz: bc47bd22bfd34e73f62bc3869b1e6578441712e6eed93d440d8ed57fb0192594
3
+ metadata.gz: 528e1dc7d30d7cc16b1507b9649429a03196747fe18b27086c23cbdfe9b85147
4
+ data.tar.gz: ff9eae6c20639f631b0fce5b3de912a73d76bb1f0e77a7e16b32fac2d645cbbc
5
5
  SHA512:
6
- metadata.gz: f44199c91669e5a544a0eba5a069ff91e46fa1b725d474a4966d6d3ab6633a716ac4cff781d16569529f73f8860926e0cca7a9c0c8256177987634f8d08f624f
7
- data.tar.gz: 8798de80e9d97a7c0f5c874c7b9671468f9152a5a445c0ccb8d3d61cbfb3f7479af32ae8587847be31d12ab4ba53433d4d5a41a6690e3a12845a9cb33962411a
6
+ metadata.gz: 7ee327ee87f60a1d8865d6abb3b1dc322a28bdc57b7a6d31f06a7d1af17425ed8e7d6af4178417d8db5ad9e84cd6794ae4a2612c1ebd65dfb4968f7b3d1e1e35
7
+ data.tar.gz: eb42c512f19919608444053fb33be11229e7968a1e6bb231b82ec9415d528baabe83051d41ba5a0335afbc0a2ec6b428c4dd420c29a03d4afcf812667b34cad0
data/LICENSE CHANGED
File without changes
data/README.md CHANGED
@@ -18,3 +18,51 @@ Or install it yourself as:
18
18
  ```bash
19
19
  $ gem install code0-zero_track
20
20
  ```
21
+
22
+ ## Features
23
+
24
+ ### `Code0::ZeroTrack::Context`
25
+
26
+ Context allows you to save data in a thread local object. Data from the Context is merged into the
27
+ log messages, if `Code0::ZeroTrack::Logs::JsonFormatter` or `Code0::ZeroTrack::Logs::JsonFormatter::Tagged`
28
+ is used.
29
+
30
+ `.with_context(data, &block)` creates a new context inheriting data from the previous context and adds the
31
+ passed data to it. The new context is dropped after the block finished execution. \
32
+ `.push(data)` creates a new context inheriting data from the previous context and adds the passed data to it. \
33
+ `.current` returns the context from the top of the stack.
34
+
35
+ ### `Code0::ZeroTrack::Memoize`
36
+
37
+ This module can be included to get access to the `memoize(name, reset_on_change, &block)` method.
38
+
39
+ This method allows to memoize a value, so it only gets computed once.
40
+ Each memoize is identified by the name. You can pass a proc to `reset_on_change` and the memoization
41
+ will automatically clear every time returned value changes.
42
+
43
+ `memoized?(name)` allows to check if a value for the given name is currently memoized. \
44
+ Memoizations can be cleared with `clear_memoize(name)` or `clear_memoize!(name)`.
45
+
46
+ ### `config.zero_track.active_record.schema_cleaner`
47
+
48
+ When using `config.active_record.schema_format = :sql`, Rails produces a `db/structure.sql`.
49
+ This file contains a lot of noise that doesn't provide much value.
50
+
51
+ This noise can be cleaned out with `config.zero_track.active_record.schema_cleaner = true`.
52
+
53
+ ### `config.zero_track.active_record.timestamps`
54
+
55
+ Setting `config.zero_track.active_record.timestamps = true` adds `timestamps_with_timezone`
56
+ and `datetime_with_timezone` as methods on the table model when creating tables in migrations.
57
+
58
+ They behave just like `timestamps` and `datetime`, just including timezones.
59
+
60
+ ### `config.zero_track.active_record.schema_migrations`
61
+
62
+ Rails uses the `schema_migrations` table to keep track which migrations have been executed.
63
+ This information is also persisted in the `db/structure.sql`, so the `schema_migrations` table
64
+ can be filled with the correct entries when the schema is loaded from the schema file.
65
+
66
+ This approach is prone to git conflicts, so you can switch to a file based persistence
67
+ with `config.zero_track.active_record.schema_migrations = true`. Instead of an `INSERT INTO` in
68
+ the `db/structure.sql`, this mode creates files in the `db/schema_migrations` directory.
data/Rakefile CHANGED
File without changes
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ class Context
6
+ LOG_KEY = 'meta'
7
+ CORRELATION_ID_KEY = 'correlation_id'
8
+ RAW_KEYS = [CORRELATION_ID_KEY].freeze
9
+
10
+ class << self
11
+ def with_context(attributes = {})
12
+ context = push(attributes)
13
+
14
+ begin
15
+ yield(context)
16
+ ensure
17
+ pop(context)
18
+ end
19
+ end
20
+
21
+ def push(new_attributes = {})
22
+ new_context = current&.merge(new_attributes) || new(new_attributes)
23
+
24
+ contexts.push(new_context)
25
+
26
+ new_context
27
+ end
28
+
29
+ def pop(context)
30
+ contexts.pop while contexts.include?(context)
31
+ end
32
+
33
+ def correlation_id
34
+ current&.correlation_id
35
+ end
36
+
37
+ def current
38
+ contexts.last
39
+ end
40
+
41
+ def log_key(key)
42
+ key = key.to_s
43
+ return key if RAW_KEYS.include?(key)
44
+ return key if key.start_with?("#{LOG_KEY}.")
45
+
46
+ "#{LOG_KEY}.#{key}"
47
+ end
48
+
49
+ private
50
+
51
+ def contexts
52
+ Thread.current[:labkit_contexts] ||= []
53
+ end
54
+ end
55
+
56
+ def initialize(values = {})
57
+ @data = {}
58
+
59
+ assign_attributes(values)
60
+ end
61
+
62
+ def merge(new_attributes)
63
+ new_context = self.class.new(data.dup)
64
+ new_context.assign_attributes(new_attributes)
65
+
66
+ new_context
67
+ end
68
+
69
+ def to_h
70
+ expand_data
71
+ end
72
+
73
+ def [](key)
74
+ to_h[log_key(key)]
75
+ end
76
+
77
+ def correlation_id
78
+ data[CORRELATION_ID_KEY]
79
+ end
80
+
81
+ def get_attribute(attribute)
82
+ raw = call_or_value(data[log_key(attribute)])
83
+
84
+ call_or_value(raw)
85
+ end
86
+
87
+ protected
88
+
89
+ def assign_attributes(attributes)
90
+ attributes = attributes.transform_keys(&method(:log_key))
91
+
92
+ data.merge!(attributes)
93
+
94
+ # Remove keys that had their values set to `nil` in the new attributes
95
+ data.keep_if { |_, value| valid_data?(value) }
96
+
97
+ # Assign a correlation if it was missing in the first context or when
98
+ # explicitly removed
99
+ data[CORRELATION_ID_KEY] ||= new_id
100
+
101
+ data
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :data
107
+
108
+ def log_key(key)
109
+ self.class.log_key(key)
110
+ end
111
+
112
+ def call_or_value(value)
113
+ value.respond_to?(:call) ? value.call : value
114
+ end
115
+
116
+ def expand_data
117
+ data.transform_values do |value|
118
+ value = call_or_value(value)
119
+
120
+ value if valid_data?(value)
121
+ end.compact
122
+ end
123
+
124
+ def new_id
125
+ SecureRandom.hex
126
+ end
127
+
128
+ def valid_data?(value)
129
+ value == false || value.present?
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ module ColumnMethods
7
+ module Timestamps
8
+ # Appends columns `created_at` and `updated_at` to a table.
9
+ #
10
+ # It is used in table creation like:
11
+ # create_table 'users' do |t|
12
+ # t.timestamps_with_timezone
13
+ # end
14
+ def timestamps_with_timezone(**options)
15
+ options[:null] = false if options[:null].nil?
16
+
17
+ %i[created_at updated_at].each do |column_name|
18
+ column(column_name, :datetime_with_timezone, **options)
19
+ end
20
+ end
21
+
22
+ # Adds specified column with appropriate timestamp type
23
+ #
24
+ # It is used in table creation like:
25
+ # create_table 'users' do |t|
26
+ # t.datetime_with_timezone :did_something_at
27
+ # end
28
+ def datetime_with_timezone(column_name, **options)
29
+ column(column_name, :datetime_with_timezone, **options)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ class Migration
7
+ # rubocop:disable Naming/ClassAndModuleCamelCase
8
+ class V1_0 < ::ActiveRecord::Migration[7.1]
9
+ include Database::MigrationHelpers::AddColumnEnhancements
10
+ include Database::MigrationHelpers::ConstraintHelpers
11
+ include Database::MigrationHelpers::IndexHelpers
12
+ include Database::MigrationHelpers::TableEnhancements
13
+ end
14
+ # rubocop:enable Naming/ClassAndModuleCamelCase
15
+
16
+ def self.[](version)
17
+ version = version.to_s
18
+ name = "V#{version.tr('.', '_')}"
19
+ raise ArgumentError, "Invalid migration version: #{version}" unless const_defined?(name, false)
20
+
21
+ const_get(name, false)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ module MigrationHelpers
7
+ module AddColumnEnhancements
8
+ def add_column(table_name, column_name, type, *args, **kwargs, &block)
9
+ helper_context = self
10
+
11
+ limit = kwargs.delete(:limit)
12
+ unique = kwargs.delete(:unique)
13
+
14
+ super
15
+
16
+ return unless type == :text
17
+
18
+ quoted_column_name = helper_context.quote_column_name(column_name)
19
+
20
+ if limit
21
+ name = helper_context.send(:text_limit_name, table_name, column_name)
22
+
23
+ definition = "char_length(#{quoted_column_name}) <= #{limit}"
24
+
25
+ add_check_constraint(table_name, definition, name: name)
26
+ end
27
+
28
+ if unique.is_a?(Hash)
29
+ unique[:where] = "#{column_name} IS NOT NULL" if unique.delete(:allow_nil_duplicate)
30
+ column_name = "LOWER(#{quoted_column_name})" if unique.delete(:case_insensitive)
31
+
32
+ add_index table_name, column_name, unique: true, **unique
33
+ elsif unique
34
+ add_index table_name, column_name, unique: unique
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ module MigrationHelpers
7
+ module ConstraintHelpers
8
+ def text_limit_name(table, column, name: nil)
9
+ name.presence || check_constraint_name(table, column, 'max_length')
10
+ end
11
+
12
+ def check_constraint_name(table, column, type)
13
+ identifier = "#{table}_#{column}_check_#{type}"
14
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
15
+
16
+ "check_#{hashed_identifier}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ module MigrationHelpers
7
+ module IndexHelpers
8
+ def index_name(table, column, type)
9
+ identifier = "#{table}_#{column}_index_#{type}"
10
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
11
+
12
+ "index_#{hashed_identifier}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Database
6
+ module MigrationHelpers
7
+ module TableEnhancements
8
+ def create_table(table_name, *args, **kwargs, &block)
9
+ helper_context = self
10
+
11
+ super do |t|
12
+ enhance(t, table_name, helper_context, &block)
13
+ end
14
+ end
15
+
16
+ def change_table(table_name, *args, **kwargs, &block)
17
+ helper_context = self
18
+
19
+ super do |t|
20
+ enhance(t, table_name, helper_context, &block)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def enhance(t, table_name, helper_context, &block)
27
+ t.define_singleton_method(:text) do |column_name, **inner_kwargs|
28
+ limit = inner_kwargs.delete(:limit)
29
+ unique = inner_kwargs.delete(:unique)
30
+
31
+ super(column_name, **inner_kwargs)
32
+
33
+ quoted_column_name = helper_context.quote_column_name(column_name)
34
+
35
+ if limit
36
+ name = helper_context.send(:text_limit_name, table_name, column_name)
37
+
38
+ definition = "char_length(#{quoted_column_name}) <= #{limit}"
39
+
40
+ t.check_constraint(definition, name: name)
41
+ end
42
+
43
+ if unique.is_a?(Hash)
44
+ index_definition = column_name
45
+ unique[:where] = "#{column_name} IS NOT NULL" if unique.delete(:allow_nil_duplicate)
46
+ index_definition = "LOWER(#{quoted_column_name})" if unique.delete(:case_insensitive)
47
+
48
+ t.index index_definition, unique: true, **unique
49
+ elsif unique
50
+ t.index column_name, unique: unique
51
+ end
52
+ end
53
+
54
+ t.define_singleton_method(:integer) do |column_name, **inner_kwargs|
55
+ unique = inner_kwargs.delete(:unique)
56
+
57
+ super(column_name, **inner_kwargs)
58
+
59
+ t.index column_name, unique: unique unless unique.nil?
60
+ end
61
+
62
+ return if block.nil?
63
+
64
+ t.instance_eval do |obj|
65
+ if block.arity == 1
66
+ block.call(obj)
67
+ elsif block.arity == 2
68
+ block.call(obj, helper_context)
69
+ else
70
+ raise ArgumentError, "Unsupported arity of #{block.arity}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ module PostgresqlAdapter
14
+ module DumpSchemaVersionsMixin
15
+ extend ActiveSupport::Concern
16
+
17
+ def dump_schema_information
18
+ # rubocop:disable Rails/SkipsModelValidations -- not an active record object
19
+ Database::SchemaMigrations.touch_all(self) unless Rails.env.production?
20
+ # rubocop:enable Rails/SkipsModelValidations
21
+ nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ module PostgresqlDatabaseTasks
14
+ module LoadSchemaVersionsMixin
15
+ extend ActiveSupport::Concern
16
+
17
+ def structure_load(...)
18
+ super
19
+
20
+ Database::SchemaMigrations.load_all(connection)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/schema_cleaner.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ class SchemaCleaner
14
+ attr_reader :original_schema
15
+
16
+ def initialize(original_schema)
17
+ @original_schema = original_schema
18
+ end
19
+
20
+ def clean(io)
21
+ structure = original_schema.dup
22
+
23
+ # Remove noise
24
+ structure.gsub!(/^COMMENT ON EXTENSION.*/, '')
25
+ structure.gsub!(/^SET.+/, '')
26
+ structure.gsub!(/^SELECT pg_catalog\.set_config\('search_path'.+/, '')
27
+ structure.gsub!(/^--.*/, "\n")
28
+
29
+ # We typically don't assume we're working with the public schema.
30
+ # pg_dump uses fully qualified object names though, since we have multiple schemas
31
+ # in the database.
32
+ #
33
+ # The intention here is to not introduce an assumption about the standard schema,
34
+ # unless we have a good reason to do so.
35
+ structure.gsub!(/public\.(\w+)/, '\1')
36
+ structure.gsub!(
37
+ /CREATE EXTENSION IF NOT EXISTS (\w+) WITH SCHEMA public;/,
38
+ 'CREATE EXTENSION IF NOT EXISTS \1;'
39
+ )
40
+
41
+ structure.gsub!(/\n{3,}/, "\n\n")
42
+
43
+ io << structure.strip
44
+ io << "\n"
45
+
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/schema_migrations/context.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ module SchemaMigrations
14
+ class Context
15
+ attr_reader :connection
16
+
17
+ class_attribute :default_schema_migrations_path, default: 'db/schema_migrations'
18
+
19
+ def initialize(connection)
20
+ @connection = connection
21
+ end
22
+
23
+ def schema_directory
24
+ @schema_directory ||= Rails.root.join(database_schema_migrations_path).to_s
25
+ end
26
+
27
+ def versions_to_create
28
+ versions_from_database = @connection.pool.schema_migration.versions
29
+ versions_from_migration_files = @connection.pool.migration_context.migrations.map { |m| m.version.to_s }
30
+
31
+ versions_from_database & versions_from_migration_files
32
+ end
33
+
34
+ private
35
+
36
+ def database_name
37
+ @database_name ||= @connection.pool.db_config.name
38
+ end
39
+
40
+ def database_schema_migrations_path
41
+ @connection.pool.db_config.configuration_hash[:schema_migrations_path] ||
42
+ self.class.default_schema_migrations_path
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/schema_migrations/migrations.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ module SchemaMigrations
14
+ class Migrations
15
+ MIGRATION_VERSION_GLOB = '20[0-9][0-9]*'
16
+
17
+ def initialize(context)
18
+ @context = context
19
+ end
20
+
21
+ def touch_all
22
+ return unless @context.versions_to_create.any?
23
+
24
+ version_filepaths = version_filenames.map { |f| File.join(schema_directory, f) }
25
+ FileUtils.rm(version_filepaths)
26
+
27
+ @context.versions_to_create.each do |version|
28
+ version_filepath = File.join(schema_directory, version)
29
+
30
+ File.open(version_filepath, 'w') do |file|
31
+ file << Digest::SHA256.hexdigest(version)
32
+ end
33
+ end
34
+ end
35
+
36
+ def load_all
37
+ return if version_filenames.empty?
38
+ return unless @context.connection.pool.schema_migration.table_exists?
39
+
40
+ values = version_filenames.map { |vf| "('#{@context.connection.quote_string(vf)}')" }
41
+
42
+ @context.connection.execute(<<~SQL.squish)
43
+ INSERT INTO schema_migrations (version)
44
+ VALUES #{values.join(',')}
45
+ ON CONFLICT DO NOTHING
46
+ SQL
47
+ end
48
+
49
+ private
50
+
51
+ def schema_directory
52
+ @context.schema_directory
53
+ end
54
+
55
+ def version_filenames
56
+ @version_filenames ||= Dir.glob(MIGRATION_VERSION_GLOB, base: schema_directory)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Heavily inspired by the implementation of GitLab
4
+ # (https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/lib/gitlab/database/schema_migrations.rb)
5
+ # which is licensed under a modified version of the MIT license which can be found at
6
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/7983d2a2203aff265fae479d7c1b7066858d1265/LICENSE
7
+ #
8
+ # The code might have been modified to accommodate for the needs of this project
9
+
10
+ module Code0
11
+ module ZeroTrack
12
+ module Database
13
+ module SchemaMigrations
14
+ module_function
15
+
16
+ def touch_all(connection)
17
+ context = Database::SchemaMigrations::Context.new(connection)
18
+
19
+ # rubocop:disable Rails/SkipsModelValidations -- not an active record object
20
+ Database::SchemaMigrations::Migrations.new(context).touch_all
21
+ # rubocop:enable Rails/SkipsModelValidations
22
+ end
23
+
24
+ def load_all(connection)
25
+ context = Database::SchemaMigrations::Context.new(connection)
26
+
27
+ Database::SchemaMigrations::Migrations.new(context).load_all
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Injectors
6
+ class ActiveRecordSchemaMigrations
7
+ def self.inject!
8
+ # Patch to write version information as empty files under the db/schema_migrations directory
9
+ # This is intended to reduce potential for merge conflicts in db/structure.sql
10
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
11
+ prepend Database::PostgresqlAdapter::DumpSchemaVersionsMixin
12
+ end
13
+ # Patch to load version information from empty files under the db/schema_migrations directory
14
+ ActiveRecord::Tasks::PostgreSQLDatabaseTasks
15
+ .prepend Database::PostgresqlDatabaseTasks::LoadSchemaVersionsMixin
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Injectors
6
+ class ActiveRecordTimestamps
7
+ def self.inject!
8
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
9
+ self::NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamptz' }
10
+ end
11
+
12
+ ActiveSupport.on_load(:active_record) do
13
+ ActiveRecord::Base.time_zone_aware_types += [:datetime_with_timezone]
14
+ end
15
+
16
+ ActiveRecord::ConnectionAdapters::ColumnMethods.include ZeroTrack::Database::ColumnMethods::Timestamps
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Loggable
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def logger
10
+ Logger.new(Rails.logger, name || '<Anonymous>')
11
+ end
12
+ end
13
+
14
+ def logger
15
+ Logger.new(Rails.logger, self.class.name || '<Anonymous>')
16
+ end
17
+
18
+ class Logger
19
+ def initialize(log, clazz)
20
+ @log = log
21
+ @clazz = clazz
22
+ end
23
+
24
+ delegate :debug?, :info?, :warn?, :error?, :fatal?, :formatter, :level, to: :@log
25
+
26
+ def with_context(&block)
27
+ Code0::ZeroTrack::Context.with_context(class: @clazz, &block)
28
+ end
29
+
30
+ def debug(message)
31
+ with_context { @log.debug(message) }
32
+ end
33
+
34
+ def error(message)
35
+ with_context { @log.error(message) }
36
+ end
37
+
38
+ def warn(message)
39
+ with_context { @log.warn(message) }
40
+ end
41
+
42
+ def info(message)
43
+ with_context { @log.info(message) }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Logs
6
+ class JsonFormatter < ::Logger::Formatter
7
+ def call(severity, datetime, _progname, message)
8
+ JSON.generate(data(severity, datetime, message)) << "\n"
9
+ end
10
+
11
+ def data(severity, datetime, message)
12
+ data = {}
13
+ data[:severity] = severity
14
+ data[:time] = datetime.utc.iso8601(3)
15
+
16
+ case message
17
+ when String
18
+ data[:message] = chomp message
19
+ when Hash
20
+ data.merge!(message)
21
+ end
22
+
23
+ data.merge!(Code0::ZeroTrack::Context.current.to_h)
24
+ end
25
+
26
+ def chomp(message)
27
+ message.chomp! until message.chomp == message
28
+
29
+ message.strip
30
+ end
31
+
32
+ class Tagged < JsonFormatter
33
+ include ActiveSupport::TaggedLogging::Formatter
34
+
35
+ def tagged(*_args)
36
+ yield self # Ignore tags, they break the json layout as they are prepended to the log line
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Memoize
6
+ def memoize(name, reset_on_change: nil)
7
+ unless reset_on_change.nil?
8
+ reset_trigger = reset_on_change.call
9
+ reset_memoize = memoize("#{name}_reset_on_change") { reset_trigger }
10
+
11
+ if reset_trigger != reset_memoize
12
+ clear_memoize(name)
13
+ clear_memoize("#{name}_reset_on_change")
14
+ end
15
+ end
16
+
17
+ if memoized?(name)
18
+ instance_variable_get(ivar(name))
19
+ else
20
+ instance_variable_set(ivar(name), yield)
21
+ end
22
+ end
23
+
24
+ def memoized?(name)
25
+ instance_variable_defined?(ivar(name))
26
+ end
27
+
28
+ def clear_memoize(name)
29
+ clear_memoize!(name) if memoized?(name)
30
+ end
31
+
32
+ def clear_memoize!(name)
33
+ remove_instance_variable(ivar(name))
34
+ end
35
+
36
+ private
37
+
38
+ def ivar(name)
39
+ case name
40
+ when Symbol
41
+ name.to_s.prepend('@').to_sym
42
+ when String
43
+ :"@#{name}"
44
+ else
45
+ raise ArgumentError, "Invalid type of '#{name}'"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -3,6 +3,21 @@
3
3
  module Code0
4
4
  module ZeroTrack
5
5
  class Railtie < ::Rails::Railtie
6
+ config.zero_track = ActiveSupport::OrderedOptions.new
7
+ config.zero_track.active_record = ActiveSupport::OrderedOptions.new
8
+ config.zero_track.active_record.timestamps = false
9
+ config.zero_track.active_record.schema_migrations = false
10
+ config.zero_track.active_record.schema_cleaner = false
11
+
12
+ rake_tasks do
13
+ path = File.expand_path(__dir__)
14
+ Dir.glob("#{path}/../../tasks/**/*.rake").each { |f| load f }
15
+ end
16
+
17
+ config.after_initialize do
18
+ Injectors::ActiveRecordTimestamps.inject! if config.zero_track.active_record.timestamps
19
+ Injectors::ActiveRecordSchemaMigrations.inject! if config.zero_track.active_record.schema_migrations
20
+ end
6
21
  end
7
22
  end
8
23
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Code0
4
4
  module ZeroTrack
5
- VERSION = '0.0.0'
5
+ VERSION = '0.0.1'
6
6
  end
7
7
  end
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'code0/zero_track/version'
4
- require 'code0/zero_track/railtie'
3
+ require 'rails/railtie'
4
+
5
+ require 'zeitwerk'
6
+ loader = Zeitwerk::Loader.new
7
+ loader.tag = File.basename(__FILE__, '.rb')
8
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
9
+ loader.push_dir(File.expand_path(File.join(__dir__, '..')))
10
+ loader.setup
5
11
 
6
12
  module Code0
7
13
  module ZeroTrack
8
14
  # Your code goes here...
9
15
  end
10
16
  end
17
+
18
+ Code0::ZeroTrack::Railtie # eager load the railtie
@@ -1,6 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # desc "Explaining what the task does"
4
- # task :code0_zero_track do
5
- # # Task goes here
6
- # end
3
+ namespace :code0 do
4
+ namespace :zero_track do
5
+ namespace :db do
6
+ desc 'This adjusts and cleans db/structure.sql - it runs after db:schema:dump'
7
+ task clean_structure_sql: :environment do |task_name|
8
+ # Allow this task to be called multiple times, as happens when running db:migrate:redo
9
+ Rake::Task[task_name].reenable
10
+
11
+ next unless Rails.application.config.zero_track.active_record.schema_cleaner
12
+
13
+ ActiveRecord::Base.configurations
14
+ .configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
15
+ .each do |db_config|
16
+ structure_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config)
17
+
18
+ schema = File.read(structure_file)
19
+
20
+ File.open(structure_file, 'wb+') do |io|
21
+ Code0::ZeroTrack::Database::SchemaCleaner.new(schema).clean(io)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Inform Rake that custom tasks should be run every time rake db:schema:dump is run
27
+ Rake::Task['db:schema:dump'].enhance do
28
+ Rake::Task['code0:zero_track:db:clean_structure_sql'].invoke
29
+ end
30
+ Rake::Task['db:prepare'].enhance do
31
+ Rake::Task['code0:zero_track:db:clean_structure_sql'].invoke
32
+ end
33
+ end
34
+ end
35
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code0-zero_track
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Niklas van Schrick
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-16 00:00:00.000000000 Z
11
+ date: 2025-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 8.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.7'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-parameterized
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rspec-rails
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -122,7 +150,7 @@ dependencies:
122
150
  - - "~>"
123
151
  - !ruby/object:Gem::Version
124
152
  version: '2.30'
125
- description:
153
+ description:
126
154
  email:
127
155
  - mc.taucher2003@gmail.com
128
156
  executables: []
@@ -133,6 +161,24 @@ files:
133
161
  - README.md
134
162
  - Rakefile
135
163
  - lib/code0/zero_track.rb
164
+ - lib/code0/zero_track/context.rb
165
+ - lib/code0/zero_track/database/column_methods.rb
166
+ - lib/code0/zero_track/database/migration.rb
167
+ - lib/code0/zero_track/database/migration_helpers/add_column_enhancements.rb
168
+ - lib/code0/zero_track/database/migration_helpers/constraint_helpers.rb
169
+ - lib/code0/zero_track/database/migration_helpers/index_helpers.rb
170
+ - lib/code0/zero_track/database/migration_helpers/table_enhancements.rb
171
+ - lib/code0/zero_track/database/postgresql_adapter/dump_schema_versions_mixin.rb
172
+ - lib/code0/zero_track/database/postgresql_database_tasks/load_schema_versions_mixin.rb
173
+ - lib/code0/zero_track/database/schema_cleaner.rb
174
+ - lib/code0/zero_track/database/schema_migrations.rb
175
+ - lib/code0/zero_track/database/schema_migrations/context.rb
176
+ - lib/code0/zero_track/database/schema_migrations/migrations.rb
177
+ - lib/code0/zero_track/injectors/active_record_schema_migrations.rb
178
+ - lib/code0/zero_track/injectors/active_record_timestamps.rb
179
+ - lib/code0/zero_track/loggable.rb
180
+ - lib/code0/zero_track/logs/json_formatter.rb
181
+ - lib/code0/zero_track/memoize.rb
136
182
  - lib/code0/zero_track/railtie.rb
137
183
  - lib/code0/zero_track/version.rb
138
184
  - lib/tasks/code0/zero_track_tasks.rake
@@ -144,7 +190,7 @@ metadata:
144
190
  source_code_uri: https://github.com/code0-tech/code0-zero_track
145
191
  changelog_uri: https://github.com/code0-tech/code0-zero_track/releases
146
192
  rubygems_mfa_required: 'true'
147
- post_install_message:
193
+ post_install_message:
148
194
  rdoc_options: []
149
195
  require_paths:
150
196
  - lib
@@ -152,7 +198,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
198
  requirements:
153
199
  - - ">="
154
200
  - !ruby/object:Gem::Version
155
- version: 3.0.0
201
+ version: 3.2.0
156
202
  required_rubygems_version: !ruby/object:Gem::Requirement
157
203
  requirements:
158
204
  - - ">="
@@ -160,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
206
  version: '0'
161
207
  requirements: []
162
208
  rubygems_version: 3.4.10
163
- signing_key:
209
+ signing_key:
164
210
  specification_version: 4
165
211
  summary: Common helpers for Code0 rails applications
166
212
  test_files: []