activerecord-sqlite-types 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 873091c3ae92306fad28b8bdef1ab2895e614c057a4bc984737946f8a824e195
4
- data.tar.gz: b3fb232c0eb97337a0d0ad1b15595ed0ca86ccd2a2beaddd6ee01b5a03687980
3
+ metadata.gz: f959c0cd5e305d5ff04b053a280c8dc20d1f731f548c0b4625e6c6cd3db52887
4
+ data.tar.gz: 8401efafe0d900337d772313489636ee66343b0fefc9d88568758d45f1894daa
5
5
  SHA512:
6
- metadata.gz: 1590a06734245552d6f3ef569ba637a50d692e48e344c57dd053489b0aab96cb8d1fa5c6f478513dec2d29bda19d2becd61dbb3d6f4292ed0dc8fd3a39e33cb8
7
- data.tar.gz: 6f64ddd2a5e0ad9dfd414f97b9e0d249a21d171c92306c8b83e1faf1289f2be4807b7434a59bbe08869b4f1e1deb53ca89cd842528743c96cff075160e3364f4
6
+ metadata.gz: a986c9134475c7b79a7cde4c7d5e7ee0e68697cb6a4f59e47ac94e3e4dbf1f40cae397a735dbb1bb093ff8599f73d64daf46085da5f6ba356fa3d295d1c2b63a
7
+ data.tar.gz: c7b6677372b165e0958b4f2c7af73a77829c2d71f21fdd93260ca3a132b19f4e7cf7e56b5037a7f112cd9cabf30c09f1e364de2044349763a9e9e9033c6fba76
data/.mutant.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ usage: opensource
3
+ includes:
4
+ - lib
5
+ - test
6
+ requires:
7
+ - sqlite_types
8
+ - generators/sqlite_types/migration/migration_generator
9
+ environment_variables:
10
+ SIMPLECOV_DISABLED: "1"
11
+ integration:
12
+ name: minitest
13
+ jobs: 1
14
+ mutation:
15
+ timeout: 30.0
16
+ matcher:
17
+ subjects:
18
+ - SQLiteTypes*
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-05-14
4
+
5
+ ### Added
6
+
7
+ - Add Rails migration generator for PostgreSQL-to-SQLite type preparation migrations
8
+ - Add migration helpers for reversible `inet`, `interval`, and PostgreSQL array conversions
9
+ - Add exact `text[]` rollback support and nested-array rollback validation for PostgreSQL migrations
10
+ - Add integration tests for ActiveRecord persistence, querying, migration data preservation, and generator output
11
+ - Preserve PostgreSQL array `NULL` elements across SQLite JSON-backed array types and migration rollbacks
12
+ - Preserve PostgreSQL array element order during JSON-to-array rollback conversions
13
+ - Add PostgreSQL rollback preflight validation for incompatible JSON array elements before schema changes
14
+ - Add PostgreSQL rollback preflight validation for the actual target array casts before schema changes
15
+ - Lock affected PostgreSQL array tables during rollback validation and restoration when a migration transaction is open
16
+ - Preserve SQL `NULL` array elements when restoring PostgreSQL `json`/`jsonb` arrays
17
+ - Reject invalid nested JSON array shapes without relying on PostgreSQL scalar array-length errors
18
+ - Serialize `:datetime` arrays in PostgreSQL-compatible timestamp JSON format for migrated-row equality queries
19
+ - Allow JSON-backed array subtypes to store broader JSON values after migration with best-effort subtype casting
20
+ - Reject JSON array numeric values that Rails would serialize as strings or `null`
21
+ - Match PostgreSQL `inet` casting for blank and invalid string assignments by casting them to `nil`
22
+
3
23
  ## [0.2.0] - 2025-10-24
4
24
 
5
25
  ### Fixed
data/README.md CHANGED
@@ -18,11 +18,45 @@ And then execute:
18
18
  bundle install
19
19
  ```
20
20
 
21
+ ## Migration Generator
22
+
23
+ Generate a rollback-aware type-preparation migration while your application is still running on PostgreSQL:
24
+
25
+ ```bash
26
+ bin/rails generate sqlite_types:migration prepare_sqlite_types \
27
+ --inet users.current_sign_in_ip users.last_sign_in_ip \
28
+ --interval timings.time_offset \
29
+ --array events.relationship_statuses:string event_classes.summary_points:text:nested email_notification_logs.attachments:hash
30
+ ```
31
+
32
+ The generator creates a migration that includes `SQLiteTypes::MigrationHelpers`:
33
+
34
+ ```ruby
35
+ class PrepareSqliteTypes < ActiveRecord::Migration[7.1]
36
+ include SQLiteTypes::MigrationHelpers
37
+
38
+ def change
39
+ change_inet_to_string :users, :current_sign_in_ip
40
+ change_inet_to_string :users, :last_sign_in_ip
41
+ change_interval_to_string :timings, :time_offset
42
+ change_array_to_json :events, :relationship_statuses, :string
43
+ change_array_to_json :event_classes, :summary_points, :text, nested: true
44
+ change_array_to_json :email_notification_logs, :attachments, :hash
45
+ end
46
+ end
47
+ ```
48
+
49
+ The generated migration is PostgreSQL-only. Run it while the application is still backed by PostgreSQL, then keep the app on PostgreSQL long enough to verify the SQLite-compatible column types with your normal Rails code, staging traffic, and test suite. After that, copy the prepared data to SQLite with a separate data migration tool.
50
+
51
+ On the way up, `change_array_to_json` converts PostgreSQL array columns to `jsonb` and adds a `jsonb_typeof(...) = 'array'` check constraint. On PostgreSQL rollback, it rebuilds the original array column through a temporary column so data compatible with the original PostgreSQL type remains reversible. Nullability is preserved unless you pass `null:` explicitly. The default assumes PostgreSQL-style empty-array defaults; if a column has different default semantics, edit the generated migration and pass `default:` explicitly. Use `:text` when the original column was `text[]`; `:string` restores a Rails `string`/`varchar[]` column. Array element order and SQL `NULL` elements are preserved. Rollbacks lock affected array tables when running inside Rails' default PostgreSQL migration transaction, preflight JSON shape, element compatibility, and the actual PostgreSQL target casts before changing schema, then raise `ActiveRecord::IrreversibleMigration` for incompatible values. Nested PostgreSQL arrays must remain rectangular with non-empty inner arrays because PostgreSQL multidimensional arrays cannot represent ragged JSON arrays or preserve empty inner arrays.
52
+
21
53
  ## Available Types
22
54
 
23
55
  ### IpAddress
24
56
 
25
57
  Replaces PostgreSQL's `inet` type with a string representation that preserves IP address functionality.
58
+ The Ruby value is an `IPAddr`, matching Rails' PostgreSQL `inet` type. `IPAddr` normalizes CIDR host bits when casting values like `192.0.2.15/24`; the migration helper preserves existing database text during the SQL type change, but new model assignments follow Rails' `IPAddr` semantics.
59
+ Blank or invalid string assignments cast to `nil`, matching Rails' PostgreSQL `inet` type. String query values are validated and then kept as written, so queries against migrated rows with preserved host bits match PostgreSQL `inet` text behavior. Invalid strings passed directly to query/persistence serialization are rejected instead of being stored as text.
26
60
 
27
61
  **Usage:**
28
62
 
@@ -36,15 +70,12 @@ end
36
70
  **Migration:**
37
71
 
38
72
  ```ruby
39
- class MigrateInetToString < ActiveRecord::Migration[7.0]
40
- def up
41
- change_column :users, :current_sign_in_ip, :string
42
- change_column :users, :last_sign_in_ip, :string
43
- end
73
+ class MigrateInetToString < ActiveRecord::Migration[7.1]
74
+ include SQLiteTypes::MigrationHelpers
44
75
 
45
- def down
46
- change_column :users, :current_sign_in_ip, :inet, using: 'current_sign_in_ip::inet'
47
- change_column :users, :last_sign_in_ip, :inet, using: 'last_sign_in_ip::inet'
76
+ def change
77
+ change_inet_to_string :users, :current_sign_in_ip
78
+ change_inet_to_string :users, :last_sign_in_ip
48
79
  end
49
80
  end
50
81
  ```
@@ -53,7 +84,11 @@ end
53
84
 
54
85
  Replaces PostgreSQL arrays with JSON-backed arrays, supporting querying via SQLite's JSON functions.
55
86
 
56
- **Supported subtypes:** `:integer`, `:string`, `:hash`, `:datetime`
87
+ **Supported subtypes:** `:integer`, `:string`, `:text`, `:hash`, `:json`, `:jsonb`, `:datetime`
88
+
89
+ All subtypes preserve `nil` elements, matching PostgreSQL array `NULL` elements. At runtime, `SQLiteTypes::Array` casts values only when they already fit the declared subtype; for example, integer strings become integers and datetime strings become `Time` values. Values outside the declared subtype are still allowed when they are native JSON values, because SQLite JSON storage can hold broader data after migration.
90
+
91
+ For example, `:integer` can store integers outside PostgreSQL `integer[]`/`int4[]` bounds and even non-integer JSON values; those values are valid for SQLite but can make a future rollback to the original PostgreSQL column type fail cleanly before schema changes. The same broader-value rule applies to `:string`, `:text`, `:hash`, and `:datetime`; rollback restores `:string` and `:text` through PostgreSQL text conversion, while incompatible values in other subtypes can block rollback. `:datetime` serializes time values in the same timestamp string shape PostgreSQL `to_jsonb(timestamp[])` uses, so equality queries can match migrated rows. The `:json` and `:jsonb` subtypes accept native JSON values only; non-finite floats and Ruby numerics that Rails would encode as strings, such as `BigDecimal`, `Rational`, and `Complex`, are rejected to avoid silent type changes or `null` writes.
57
92
 
58
93
  **Usage:**
59
94
 
@@ -68,15 +103,12 @@ end
68
103
  **Migration:**
69
104
 
70
105
  ```ruby
71
- class MigrateArrayToJson < ActiveRecord::Migration[7.0]
72
- def up
73
- change_column :users, :personality_traits, :json
74
- change_column :users, :favorite_numbers, :json
75
- end
106
+ class MigrateArrayToJson < ActiveRecord::Migration[7.1]
107
+ include SQLiteTypes::MigrationHelpers
76
108
 
77
- def down
78
- change_column :users, :personality_traits, :text, array: true, default: [], using: 'personality_traits::text[]'
79
- change_column :users, :favorite_numbers, :integer, array: true, default: [], using: 'favorite_numbers::integer[]'
109
+ def change
110
+ change_array_to_json :users, :personality_traits, :string
111
+ change_array_to_json :users, :favorite_numbers, :integer
80
112
  end
81
113
  end
82
114
  ```
@@ -88,6 +120,7 @@ For array querying functionality, see [Stephen Margheim's article on enhancing R
88
120
  ### Interval
89
121
 
90
122
  Replaces PostgreSQL's `interval` type with ISO8601 duration strings.
123
+ Fixed-length `ActiveSupport::Duration` values are serialized through their total seconds, so equivalent durations like `150.minutes` and `2.hours + 30.minutes` use the same queryable representation. Variable durations with month or year parts keep their calendar components.
91
124
 
92
125
  **Usage:**
93
126
 
@@ -100,25 +133,23 @@ end
100
133
  **Migration:**
101
134
 
102
135
  ```ruby
103
- class MigrateIntervalToString < ActiveRecord::Migration[7.0]
104
- def up
105
- change_column :events, :duration, :string
106
- end
136
+ class MigrateIntervalToString < ActiveRecord::Migration[7.1]
137
+ include SQLiteTypes::MigrationHelpers
107
138
 
108
- def down
109
- change_column :events, :duration, :interval, using: 'duration::interval'
139
+ def change
140
+ change_interval_to_string :events, :duration
110
141
  end
111
142
  end
112
143
  ```
113
144
 
114
145
  ## Migration Strategy
115
146
 
116
- The recommended approach for migrating from PostgreSQL to SQLite is incremental and reversible:
147
+ The recommended approach for migrating from PostgreSQL to SQLite is incremental and reversible while data remains compatible with the original PostgreSQL column types:
117
148
 
118
149
  1. **Prepare while still on PostgreSQL:**
119
150
  - Add custom type declarations to your models
120
151
  - Run migrations to change column types (e.g., `inet` → `string`)
121
- - Test thoroughly - migrations are reversible!
152
+ - Test thoroughly; rollbacks are preflighted and reversible for compatible data.
122
153
 
123
154
  2. **Switch to SQLite:**
124
155
  - Update `database.yml` to point to SQLite
@@ -133,7 +164,7 @@ For a detailed migration guide, see [this presentation on migrating from Postgre
133
164
 
134
165
  ## Development
135
166
 
136
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
167
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests with SimpleCov's 100% line and branch coverage gates. Run `bundle exec standardrb` for style checks and `bundle exec mutant run` for the mutation test suite. The default `rake` task runs all three gates.
137
168
 
138
169
  To install this gem onto your local machine, run `bundle exec rake install`.
139
170
 
@@ -1,8 +1,12 @@
1
1
  require "active_record"
2
+ require "active_support/time"
3
+ require "date"
4
+ require "ipaddr"
2
5
  require_relative "sqlite_types/version"
3
6
  require_relative "sqlite_types/ip_address"
4
7
  require_relative "sqlite_types/array"
5
8
  require_relative "sqlite_types/interval"
9
+ require_relative "sqlite_types/migration_helpers"
6
10
 
7
11
  module SQLiteTypes
8
12
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+ require "sqlite_types/migration_helpers"
6
+
7
+ module SQLiteTypes
8
+ module Generators
9
+ class MigrationGenerator < Rails::Generators::NamedBase
10
+ include ActiveRecord::Generators::Migration
11
+
12
+ IDENTIFIER_PATTERN = /\A[a-zA-Z_]\w*\z/
13
+ ARRAY_MODIFIERS = %w[nested].freeze
14
+
15
+ namespace "sqlite_types:migration"
16
+ source_root File.expand_path("templates", __dir__)
17
+
18
+ class_option :inet,
19
+ type: :array,
20
+ default: [],
21
+ banner: "table.column",
22
+ desc: "inet columns to migrate to string"
23
+ class_option :interval,
24
+ type: :array,
25
+ default: [],
26
+ banner: "table.column",
27
+ desc: "interval columns to migrate to string"
28
+ class_option :array,
29
+ type: :array,
30
+ default: [],
31
+ banner: "table.column:subtype[:nested]",
32
+ desc: "PostgreSQL array columns to migrate to json/jsonb"
33
+
34
+ def self.next_migration_number(dirname)
35
+ if timestamped_migrations?
36
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
37
+ else
38
+ "%.3d" % (current_migration_number(dirname) + 1)
39
+ end
40
+ end
41
+
42
+ def self.timestamped_migrations?
43
+ return ActiveRecord.timestamped_migrations if ActiveRecord.respond_to?(:timestamped_migrations)
44
+ return ActiveRecord::Base.timestamped_migrations if ActiveRecord::Base.respond_to?(:timestamped_migrations)
45
+
46
+ true
47
+ end
48
+
49
+ def create_migration_file
50
+ validate_column_options!
51
+
52
+ migration_template "migration.rb.tt", "db/migrate/#{file_name}.rb"
53
+ end
54
+
55
+ private
56
+
57
+ def validate_column_options!
58
+ return if inet_columns.any? || interval_columns.any? || array_columns.any?
59
+
60
+ raise Rails::Generators::Error, "Provide at least one --inet, --interval, or --array column"
61
+ end
62
+
63
+ def migration_version
64
+ ActiveRecord::Migration.current_version
65
+ end
66
+
67
+ def inet_columns
68
+ @inet_columns ||= Array(options[:inet]).map { |spec| parse_column_spec(spec) }
69
+ end
70
+
71
+ def interval_columns
72
+ @interval_columns ||= Array(options[:interval]).map { |spec| parse_column_spec(spec) }
73
+ end
74
+
75
+ def array_columns
76
+ @array_columns ||= Array(options[:array]).map { |spec| parse_array_spec(spec) }
77
+ end
78
+
79
+ def parse_column_spec(spec)
80
+ table_name, column_name = spec.to_s.split(".", 2)
81
+ raise Rails::Generators::Error, "Expected column as table.column, got #{spec.inspect}" if table_name.empty? || column_name.to_s.empty?
82
+ validate_identifier! table_name, "table"
83
+ validate_identifier! column_name, "column"
84
+
85
+ ColumnSpec.new(table_name: table_name, column_name: column_name)
86
+ end
87
+
88
+ def parse_array_spec(spec)
89
+ column_spec, subtype, *modifiers = spec.to_s.split(":")
90
+ raise Rails::Generators::Error, "Expected array column as table.column:subtype, got #{spec.inspect}" if subtype.to_s.empty?
91
+ validate_array_subtype! subtype
92
+ validate_array_modifiers! modifiers
93
+
94
+ column = parse_column_spec(column_spec)
95
+ ArraySpec.new(
96
+ table_name: column.table_name,
97
+ column_name: column.column_name,
98
+ subtype: subtype,
99
+ nested: modifiers.include?("nested")
100
+ )
101
+ end
102
+
103
+ def validate_identifier!(value, label)
104
+ return if value.match?(IDENTIFIER_PATTERN)
105
+
106
+ raise Rails::Generators::Error, "Invalid #{label} identifier: #{value.inspect}"
107
+ end
108
+
109
+ def validate_array_subtype!(subtype)
110
+ return if SQLiteTypes::MigrationHelpers::SUPPORTED_ARRAY_SUBTYPES.include?(subtype.to_sym)
111
+
112
+ raise Rails::Generators::Error, "Unsupported array subtype: #{subtype.inspect}"
113
+ end
114
+
115
+ def validate_array_modifiers!(modifiers)
116
+ unknown_modifiers = modifiers - ARRAY_MODIFIERS
117
+ return if unknown_modifiers.empty?
118
+
119
+ raise Rails::Generators::Error, "Unsupported array modifier: #{unknown_modifiers.first.inspect}"
120
+ end
121
+
122
+ ColumnSpec = Struct.new(:table_name, :column_name, keyword_init: true)
123
+ ArraySpec = Struct.new(:table_name, :column_name, :subtype, :nested, keyword_init: true)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sqlite_types/migration_helpers"
4
+
5
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= migration_version %>]
6
+ include SQLiteTypes::MigrationHelpers
7
+
8
+ def change
9
+ <% inet_columns.each do |column| -%>
10
+ change_inet_to_string :<%= column.table_name %>, :<%= column.column_name %>
11
+ <% end -%>
12
+ <% interval_columns.each do |column| -%>
13
+ change_interval_to_string :<%= column.table_name %>, :<%= column.column_name %>
14
+ <% end -%>
15
+ <% array_columns.each do |column| -%>
16
+ change_array_to_json :<%= column.table_name %>, :<%= column.column_name %>, :<%= column.subtype %><%= ", nested: true" if column.nested %>
17
+ <% end -%>
18
+ end
19
+ end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  module SQLiteTypes
4
6
  class Array < ActiveRecord::Type::Json
5
- SUPPORTED_SUBTYPES = %i[integer string hash datetime].freeze
7
+ SUPPORTED_SUBTYPES = %i[integer string text hash json jsonb datetime].freeze
8
+ POSTGRESQL_TIMESTAMP_JSON_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
6
9
 
7
10
  def initialize(subtype, nested: false)
11
+ subtype = subtype.to_sym if subtype.respond_to?(:to_sym)
8
12
  raise ArgumentError, "Unsupported subtype: #{subtype}" unless SUPPORTED_SUBTYPES.include?(subtype)
9
13
 
10
14
  @subtype = subtype
@@ -15,19 +19,25 @@ module SQLiteTypes
15
19
  def deserialize(value)
16
20
  return if value.nil?
17
21
 
18
- array = parse_to_array(value)
22
+ array = parse_array(value)
19
23
 
20
24
  if @nested
21
- array.map { |nested_array| parse_to_array(nested_array).map { |element| cast_element(element) } }
25
+ array.map do |nested_array|
26
+ parse_array(nested_array, nested: true).map { |element| cast_element(element) }
27
+ end
22
28
  else
23
29
  array.map { |element| cast_element(element) }
24
30
  end
25
31
  end
26
32
 
27
33
  def serialize(value)
28
- raise ArgumentError, "Invalid value: #{value}" if !valid?(value)
34
+ return super if value.nil?
29
35
 
30
- super
36
+ # Normalize through the same cast path as reads so persisted values and equality queries
37
+ # use one canonical JSON representation for the declared subtype.
38
+ array = deserialize(value)
39
+ array = serialize_datetime_array(array) if @subtype == :datetime
40
+ super(array)
31
41
  end
32
42
 
33
43
  # Use "=" instead of "IN" in WHERE clause, to match PostgreSQL arrays
@@ -37,41 +47,108 @@ module SQLiteTypes
37
47
 
38
48
  private
39
49
 
40
- def valid?(value)
41
- return true if value.nil?
42
- return false if !value.is_a?(::Array)
43
- return value.all? { |nested_array| nested_array.is_a?(::Array) } if @nested
44
-
45
- true
46
- end
47
-
48
- def parse_to_array(value)
49
- case value
50
+ def parse_array(value, nested: false)
51
+ array = case value
50
52
  when ::String
51
53
  begin
52
54
  parsed = ::ActiveSupport::JSON.decode(value)
53
- parsed.is_a?(::Array) ? parsed : nil
55
+ parsed if parsed.instance_of?(::Array)
54
56
  rescue JSON::ParserError
55
57
  nil
56
58
  end
57
59
  when ::Array
58
60
  value
59
61
  end
62
+
63
+ return array if array
64
+
65
+ description = nested ? "nested array" : "array"
66
+ raise ArgumentError, "Invalid #{description} value: #{value.inspect}"
60
67
  end
61
68
 
62
69
  def cast_element(elem)
70
+ raise ArgumentError, "Invalid #{@subtype} array element: #{elem.inspect}" unless storable_element?(elem)
71
+
63
72
  case @subtype
64
73
  when :integer
65
- elem.to_i
66
- when :string
67
- elem.to_s
74
+ integer_string?(elem) ? cast_integer_element(elem) : elem
75
+ when :string, :text
76
+ elem
68
77
  when :hash
69
- elem.to_h
78
+ elem.is_a?(::Hash) ? elem.to_h : elem
79
+ when :json, :jsonb
80
+ elem
70
81
  when :datetime
71
- elem.is_a?(::String) ? ::DateTime.parse(elem).in_time_zone : elem
82
+ cast_datetime_element(elem)
72
83
  else
73
84
  raise ArgumentError, "Unsupported subtype: #{@subtype}"
74
85
  end
75
86
  end
87
+
88
+ def storable_element?(elem)
89
+ return true if @subtype == :datetime && datetime_like?(elem)
90
+
91
+ json_compatible?(elem)
92
+ end
93
+
94
+ def datetime_like?(value)
95
+ value.acts_like?(:time) || value.is_a?(::Date)
96
+ end
97
+
98
+ def integer_string?(value)
99
+ value.is_a?(::String) && value.match?(/\A[+-]?\d+\z/)
100
+ end
101
+
102
+ def cast_integer_element(value)
103
+ Integer(value, 10)
104
+ end
105
+
106
+ def cast_datetime_element(value)
107
+ return value.respond_to?(:in_time_zone) ? value.in_time_zone : value if datetime_like?(value)
108
+ return value unless value.is_a?(::String)
109
+
110
+ parse_datetime(value)&.in_time_zone || value
111
+ end
112
+
113
+ def parse_datetime(value)
114
+ ::DateTime.parse(value)
115
+ rescue ArgumentError
116
+ nil
117
+ end
118
+
119
+ def serialize_datetime_array(array)
120
+ if @nested
121
+ array.map { |nested_array| nested_array.map { |element| serialize_datetime_element(element) } }
122
+ else
123
+ array.map { |element| serialize_datetime_element(element) }
124
+ end
125
+ end
126
+
127
+ def serialize_datetime_element(element)
128
+ return element unless element.acts_like?(:time)
129
+
130
+ postgresql_timestamp_json(element)
131
+ end
132
+
133
+ def postgresql_timestamp_json(element)
134
+ # PostgreSQL to_jsonb(timestamp[]) emits timestamp text without a time-zone suffix,
135
+ # and omits the fractional part when it is zero.
136
+ element.to_time.utc.strftime(POSTGRESQL_TIMESTAMP_JSON_FORMAT).sub(/(\.\d*?)0+\z/, "\\1").delete_suffix(".")
137
+ end
138
+
139
+ def json_compatible?(value)
140
+ case value
141
+ when nil, true, false, ::String, ::Integer
142
+ true
143
+ when ::Float
144
+ value.finite?
145
+ when ::Array
146
+ value.all? { |element| json_compatible?(element) }
147
+ when ::Hash
148
+ value.all? do |key, element|
149
+ (key.is_a?(::String) || key.instance_of?(::Symbol)) && json_compatible?(element)
150
+ end
151
+ end
152
+ end
76
153
  end
77
154
  end
@@ -7,7 +7,7 @@ module SQLiteTypes
7
7
  def serialize(value)
8
8
  case value
9
9
  when ::ActiveSupport::Duration
10
- value.iso8601(precision: precision)
10
+ canonical_duration(value).iso8601(precision: precision)
11
11
  when ::Numeric
12
12
  ::ActiveSupport::Duration.build(value).iso8601(precision: precision)
13
13
  else
@@ -21,10 +21,14 @@ module SQLiteTypes
21
21
 
22
22
  private
23
23
 
24
+ def canonical_duration(value)
25
+ return value if value.variable?
26
+
27
+ ::ActiveSupport::Duration.build(value)
28
+ end
29
+
24
30
  def cast_value(value)
25
31
  case value
26
- when ::ActiveSupport::Duration
27
- value
28
32
  when ::String
29
33
  begin
30
34
  ::ActiveSupport::Duration.parse(value)
@@ -7,23 +7,49 @@ module SQLiteTypes
7
7
 
8
8
  case value
9
9
  when ::IPAddr
10
- "#{value}/#{value.prefix}"
10
+ serialize_ipaddr(value)
11
11
  when ::String
12
- ip_addr = ::IPAddr.new(value)
13
- "#{ip_addr}/#{ip_addr.prefix}"
12
+ ::IPAddr.new(value)
13
+ value
14
14
  else
15
15
  raise ArgumentError, "Invalid IP address: #{value}"
16
16
  end
17
17
  end
18
18
 
19
+ def changed?(old_value, new_value, _new_value_before_type_cast)
20
+ !serialize_for_change(old_value).eql?(serialize_for_change(new_value))
21
+ end
22
+
23
+ def changed_in_place?(raw_old_value, new_value)
24
+ !serialize_for_change(raw_old_value).eql?(serialize_for_change(new_value))
25
+ rescue ArgumentError
26
+ true
27
+ end
28
+
19
29
  private
20
30
 
31
+ def serialize_ipaddr(value)
32
+ "#{value}/#{value.prefix}"
33
+ end
34
+
35
+ def serialize_for_change(value)
36
+ return if value.nil?
37
+
38
+ cast_value(value)
39
+ end
40
+
21
41
  def cast_value(value)
22
42
  case value
23
43
  when ::IPAddr
24
44
  value
25
45
  when ::String
26
- ::IPAddr.new(value)
46
+ begin
47
+ ::IPAddr.new(value)
48
+ rescue ::IPAddr::InvalidAddressError
49
+ # Rails' PostgreSQL inet type casts invalid string assignments to nil;
50
+ # serialization still raises for invalid query and persistence values.
51
+ nil
52
+ end
27
53
  else
28
54
  raise ArgumentError, "Invalid IP address: #{value}"
29
55
  end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require "digest"
5
+ require_relative "array"
6
+
7
+ module SQLiteTypes
8
+ module MigrationHelpers
9
+ DEFAULT_NOT_PROVIDED = Object.new.freeze
10
+ POSTGRESQL_IDENTIFIER_LIMIT = 63
11
+ SUPPORTED_ARRAY_SUBTYPES = SQLiteTypes::Array::SUPPORTED_SUBTYPES
12
+
13
+ def change_inet_to_string(table_name, column_name, **options)
14
+ require_postgresql_adapter!
15
+
16
+ reversible do |dir|
17
+ dir.up do
18
+ change_column table_name, column_name, :string, **options.merge(using: "#{quote_column_name(column_name)}::text")
19
+ end
20
+
21
+ dir.down do
22
+ change_column table_name, column_name, :inet, **options.merge(using: "#{quote_column_name(column_name)}::inet")
23
+ end
24
+ end
25
+ end
26
+
27
+ def change_interval_to_string(table_name, column_name, **options)
28
+ require_postgresql_adapter!
29
+
30
+ reversible do |dir|
31
+ dir.up do
32
+ change_column table_name, column_name, :string, **options.merge(using: "#{quote_column_name(column_name)}::text")
33
+ end
34
+
35
+ dir.down do
36
+ change_column table_name, column_name, :interval, **options.merge(using: "#{quote_column_name(column_name)}::interval")
37
+ end
38
+ end
39
+ end
40
+
41
+ def change_array_to_json(
42
+ table_name,
43
+ column_name,
44
+ subtype,
45
+ nested: false,
46
+ null: nil,
47
+ default: [],
48
+ constraint_name: nil
49
+ )
50
+ require_postgresql_adapter!
51
+
52
+ subtype = normalize_array_subtype(subtype)
53
+ constraint_name ||= default_array_constraint_name(table_name, column_name)
54
+ sqlite_types_array_columns << [table_name, column_name, subtype, nested]
55
+
56
+ reversible do |dir|
57
+ dir.up do
58
+ options = {}
59
+ options[:null] = null unless null.nil?
60
+
61
+ change_array_default table_name, column_name, from: default, to: nil
62
+ change_column table_name, column_name, :jsonb, **options.merge(using: "to_jsonb(#{quote_column_name(column_name)})")
63
+ change_array_default table_name, column_name, from: nil, to: default
64
+ add_check_constraint table_name, "jsonb_typeof(#{quote_column_name(column_name)}) = 'array'", name: constraint_name
65
+ end
66
+
67
+ dir.down do
68
+ validate_registered_postgresql_array_rollbacks!
69
+ remove_check_constraint table_name, name: constraint_name
70
+ restore_postgresql_array_column table_name, column_name, subtype, nested: nested, null: resolved_null(table_name, column_name, null), default: default
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def require_postgresql_adapter!
78
+ return if connection.adapter_name.downcase.include?("postgresql")
79
+
80
+ raise ActiveRecord::MigrationError,
81
+ "SQLiteTypes migration helpers must run on PostgreSQL before migrating data to SQLite"
82
+ end
83
+
84
+ def normalize_array_subtype(subtype)
85
+ subtype = subtype.to_sym if subtype.respond_to?(:to_sym)
86
+ raise ArgumentError, "Unsupported array subtype: #{subtype}" unless SUPPORTED_ARRAY_SUBTYPES.include?(subtype)
87
+
88
+ subtype
89
+ end
90
+
91
+ def change_array_default(table_name, column_name, from:, to:)
92
+ return if from.equal?(DEFAULT_NOT_PROVIDED) || to.equal?(DEFAULT_NOT_PROVIDED)
93
+ return if from == to
94
+
95
+ change_column_default table_name, column_name, from: from, to: to
96
+ end
97
+
98
+ def restore_postgresql_array_column(table_name, column_name, subtype, nested:, null:, default:)
99
+ temporary_column_name = temporary_array_column_name(column_name)
100
+
101
+ add_column table_name, temporary_column_name, postgresql_array_subtype(subtype), **postgresql_array_column_options(null: null, default: default)
102
+ execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(temporary_column_name)} = #{postgresql_array_restore_expression(quote_column_name(column_name), subtype, nested)};"
103
+ remove_column table_name, column_name
104
+ rename_column table_name, temporary_column_name, column_name
105
+ end
106
+
107
+ def postgresql_array_restore_expression(column_sql, subtype, nested)
108
+ "CASE WHEN #{column_sql} IS NULL THEN NULL ELSE COALESCE((#{postgresql_array_expression(column_sql, subtype, nested)}), '{}') END"
109
+ end
110
+
111
+ def postgresql_array_subtype(subtype)
112
+ case subtype
113
+ when :hash
114
+ :jsonb
115
+ else
116
+ subtype
117
+ end
118
+ end
119
+
120
+ def postgresql_array_expression(column_sql, subtype, nested)
121
+ if nested
122
+ inner_expression = postgresql_array_expression("sqlite_types_outer.value", subtype, false)
123
+ return "SELECT array_agg((#{inner_expression}) ORDER BY sqlite_types_outer.ordinality) FROM jsonb_array_elements(#{column_sql}) WITH ORDINALITY AS sqlite_types_outer(value, ordinality)"
124
+ end
125
+
126
+ case subtype
127
+ when :string, :text
128
+ "SELECT array_agg(sqlite_types_element.value ORDER BY sqlite_types_element.ordinality) FROM jsonb_array_elements_text(#{column_sql}) WITH ORDINALITY AS sqlite_types_element(value, ordinality)"
129
+ when :integer
130
+ "SELECT array_agg(sqlite_types_element.value::int ORDER BY sqlite_types_element.ordinality) FROM jsonb_array_elements_text(#{column_sql}) WITH ORDINALITY AS sqlite_types_element(value, ordinality)"
131
+ when :datetime
132
+ "SELECT array_agg(sqlite_types_element.value::timestamp ORDER BY sqlite_types_element.ordinality) FROM jsonb_array_elements_text(#{column_sql}) WITH ORDINALITY AS sqlite_types_element(value, ordinality)"
133
+ when :json
134
+ "SELECT array_agg(CASE WHEN jsonb_typeof(sqlite_types_element.value) = 'null' THEN NULL ELSE sqlite_types_element.value::json END ORDER BY sqlite_types_element.ordinality) FROM jsonb_array_elements(#{column_sql}) WITH ORDINALITY AS sqlite_types_element(value, ordinality)"
135
+ when :hash, :jsonb
136
+ "SELECT array_agg(CASE WHEN jsonb_typeof(sqlite_types_element.value) = 'null' THEN NULL ELSE sqlite_types_element.value END ORDER BY sqlite_types_element.ordinality) FROM jsonb_array_elements(#{column_sql}) WITH ORDINALITY AS sqlite_types_element(value, ordinality)"
137
+ else
138
+ raise ArgumentError, "Unsupported array subtype: #{subtype}"
139
+ end
140
+ end
141
+
142
+ def validate_postgresql_nested_array_shape!(table_name, column_name)
143
+ column_sql = "sqlite_types_row.#{quote_column_name(column_name)}"
144
+ invalid_row = select_value "SELECT 1 FROM #{quote_table_name(table_name)} AS sqlite_types_row WHERE #{column_sql} IS NOT NULL AND (jsonb_typeof(#{column_sql}) <> 'array' OR EXISTS (SELECT 1 FROM jsonb_array_elements(#{column_sql}) AS sqlite_types_outer(value) WHERE CASE WHEN jsonb_typeof(sqlite_types_outer.value) <> 'array' THEN true ELSE jsonb_array_length(sqlite_types_outer.value) = 0 END) OR (SELECT COUNT(DISTINCT jsonb_array_length(sqlite_types_outer.value)) FROM jsonb_array_elements(#{column_sql}) AS sqlite_types_outer(value) WHERE jsonb_typeof(sqlite_types_outer.value) = 'array') > 1) LIMIT 1"
145
+
146
+ return unless invalid_row
147
+
148
+ raise ActiveRecord::IrreversibleMigration,
149
+ "Cannot restore #{table_name}.#{column_name} to a PostgreSQL multidimensional array; nested JSON arrays must be rectangular and inner arrays must be non-empty"
150
+ end
151
+
152
+ def validate_postgresql_array_elements!(table_name, column_name, subtype, nested:)
153
+ column_sql = "sqlite_types_row.#{quote_column_name(column_name)}"
154
+ invalid_condition = postgresql_invalid_array_element_condition("sqlite_types_element.value", subtype)
155
+ invalid_row = if nested
156
+ select_value "SELECT 1 FROM #{quote_table_name(table_name)} AS sqlite_types_row WHERE #{column_sql} IS NOT NULL AND EXISTS (SELECT 1 FROM jsonb_array_elements(#{column_sql}) AS sqlite_types_outer(value) WHERE jsonb_typeof(sqlite_types_outer.value) = 'array' AND EXISTS (SELECT 1 FROM jsonb_array_elements(sqlite_types_outer.value) AS sqlite_types_element(value) WHERE #{invalid_condition})) LIMIT 1"
157
+ else
158
+ select_value "SELECT 1 FROM #{quote_table_name(table_name)} AS sqlite_types_row WHERE #{column_sql} IS NOT NULL AND EXISTS (SELECT 1 FROM jsonb_array_elements(#{column_sql}) AS sqlite_types_element(value) WHERE #{invalid_condition}) LIMIT 1"
159
+ end
160
+
161
+ return unless invalid_row
162
+
163
+ raise ActiveRecord::IrreversibleMigration,
164
+ "Cannot restore #{table_name}.#{column_name} to a PostgreSQL #{subtype} array; JSON elements are not compatible with the target array subtype"
165
+ end
166
+
167
+ def postgresql_invalid_array_element_condition(element_sql, subtype)
168
+ type_sql = "jsonb_typeof(#{element_sql})"
169
+
170
+ case subtype
171
+ when :string, :text
172
+ "false"
173
+ when :datetime
174
+ "#{type_sql} NOT IN ('string', 'null')"
175
+ when :integer
176
+ "CASE WHEN #{type_sql} = 'null' THEN false WHEN #{type_sql} IN ('number', 'string') THEN (#{element_sql} #>> '{}') !~ '^[+-]?[0-9]+$' ELSE true END"
177
+ when :hash, :json, :jsonb
178
+ "false"
179
+ else
180
+ raise ArgumentError, "Unsupported array subtype: #{subtype}"
181
+ end
182
+ end
183
+
184
+ def validate_registered_postgresql_array_rollbacks!
185
+ return if @sqlite_types_array_rollbacks_validated
186
+
187
+ lock_registered_postgresql_array_tables!
188
+
189
+ sqlite_types_array_columns.each do |table_name, column_name, subtype, nested|
190
+ validate_postgresql_array_shape!(table_name, column_name, subtype)
191
+ validate_postgresql_nested_array_shape!(table_name, column_name) if nested
192
+ validate_postgresql_array_elements!(table_name, column_name, subtype, nested: nested)
193
+ validate_postgresql_array_restore_cast!(table_name, column_name, subtype, nested: nested)
194
+ end
195
+
196
+ @sqlite_types_array_rollbacks_validated = true
197
+ end
198
+
199
+ def lock_registered_postgresql_array_tables!
200
+ return if connection.respond_to?(:transaction_open?) && !connection.transaction_open?
201
+
202
+ sqlite_types_array_columns.map(&:first).uniq.each do |table_name|
203
+ execute "LOCK TABLE #{quote_table_name(table_name)} IN SHARE ROW EXCLUSIVE MODE"
204
+ end
205
+ end
206
+
207
+ def validate_postgresql_array_shape!(table_name, column_name, subtype)
208
+ column_sql = "sqlite_types_row.#{quote_column_name(column_name)}"
209
+ invalid_row = select_value "SELECT 1 FROM #{quote_table_name(table_name)} AS sqlite_types_row WHERE #{column_sql} IS NOT NULL AND jsonb_typeof(#{column_sql}) <> 'array' LIMIT 1"
210
+
211
+ return unless invalid_row
212
+
213
+ raise ActiveRecord::IrreversibleMigration,
214
+ "Cannot restore #{table_name}.#{column_name} to a PostgreSQL #{subtype} array; JSON value is not an array"
215
+ end
216
+
217
+ def validate_postgresql_array_restore_cast!(table_name, column_name, subtype, nested:)
218
+ column_sql = "sqlite_types_row.#{quote_column_name(column_name)}"
219
+ select_value "SELECT COUNT(*) FROM #{quote_table_name(table_name)} AS sqlite_types_row WHERE #{column_sql} IS NOT NULL AND (#{postgresql_array_restore_expression(column_sql, subtype, nested)}) IS NOT NULL"
220
+ rescue ActiveRecord::StatementInvalid => error
221
+ raise ActiveRecord::IrreversibleMigration,
222
+ "Cannot restore #{table_name}.#{column_name} to a PostgreSQL #{subtype} array; PostgreSQL rejected the rollback cast: #{error.message.lines.first&.strip}"
223
+ end
224
+
225
+ def sqlite_types_array_columns
226
+ @sqlite_types_array_columns ||= []
227
+ end
228
+
229
+ def postgresql_array_column_options(null:, default:)
230
+ options = {array: true}
231
+ options[:null] = null unless null.nil?
232
+ return options if default.equal?(DEFAULT_NOT_PROVIDED)
233
+
234
+ options.merge(default: default)
235
+ end
236
+
237
+ def resolved_null(table_name, column_name, null)
238
+ return null unless null.nil?
239
+ return unless connection.respond_to?(:columns)
240
+
241
+ connection.columns(table_name).find { |column| column.name == column_name.to_s }&.null
242
+ end
243
+
244
+ def default_array_constraint_name(table_name, column_name)
245
+ truncate_identifier("#{table_name}_#{column_name}_array_check")
246
+ end
247
+
248
+ def temporary_array_column_name(column_name)
249
+ truncate_identifier("#{column_name}_sqlite_types_tmp")
250
+ end
251
+
252
+ def truncate_identifier(identifier)
253
+ identifier = identifier.to_s
254
+ return identifier if identifier.length <= POSTGRESQL_IDENTIFIER_LIMIT
255
+
256
+ digest = Digest::SHA256.hexdigest(identifier)[0, 10]
257
+ "#{identifier[0, POSTGRESQL_IDENTIFIER_LIMIT - digest.length - 1]}_#{digest}"
258
+ end
259
+ end
260
+ end
@@ -1,3 +1,3 @@
1
1
  module SQLiteTypes
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "activerecord-sqlite-types"
data/sig/sqlite_types.rbs CHANGED
@@ -1,4 +1,32 @@
1
1
  module SQLiteTypes
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class IpAddress < ActiveRecord::Type::String
5
+ def serialize: (untyped value) -> String?
6
+ def changed?: (untyped old_value, untyped new_value, untyped new_value_before_type_cast) -> bool
7
+ def changed_in_place?: (untyped raw_old_value, untyped new_value) -> bool
8
+ end
9
+
10
+ class Array < ActiveRecord::Type::Json
11
+ SUPPORTED_SUBTYPES: ::Array[Symbol]
12
+
13
+ def initialize: ((Symbol | String) subtype, ?nested: bool) -> void
14
+ def deserialize: (untyped value) -> ::Array[untyped]?
15
+ def serialize: (untyped value) -> untyped
16
+ def force_equality?: (untyped value) -> bool
17
+ end
18
+
19
+ class Interval < ActiveRecord::Type::Value
20
+ def serialize: (untyped value) -> untyped
21
+ def type_cast_for_schema: (untyped value) -> String
22
+ end
23
+
24
+ module MigrationHelpers
25
+ DEFAULT_NOT_PROVIDED: untyped
26
+ SUPPORTED_ARRAY_SUBTYPES: ::Array[Symbol]
27
+
28
+ def change_inet_to_string: ((Symbol | String) table_name, (Symbol | String) column_name, **untyped options) -> untyped
29
+ def change_interval_to_string: ((Symbol | String) table_name, (Symbol | String) column_name, **untyped options) -> untyped
30
+ def change_array_to_json: ((Symbol | String) table_name, (Symbol | String) column_name, (Symbol | String) subtype, ?nested: bool, ?null: bool, ?default: untyped, ?constraint_name: String?) -> untyped
31
+ end
4
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-sqlite-types
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wojtek Wrona
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '7.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7.1'
41
69
  description: Provides custom ActiveRecord types to handle PostgreSQL-specific data
42
70
  types (inet, interval, arrays) when migrating to SQLite in Rails applications.
43
71
  email:
@@ -46,19 +74,18 @@ executables: []
46
74
  extensions: []
47
75
  extra_rdoc_files: []
48
76
  files:
49
- - ".envrc"
50
- - ".standard.yml"
77
+ - ".mutant.yml"
51
78
  - CHANGELOG.md
52
79
  - LICENSE.txt
53
80
  - README.md
54
- - Rakefile
55
- - devenv.lock
56
- - devenv.nix
57
- - devenv.yaml
58
81
  - lib/activerecord-sqlite-types.rb
82
+ - lib/generators/sqlite_types/migration/migration_generator.rb
83
+ - lib/generators/sqlite_types/migration/templates/migration.rb.tt
84
+ - lib/sqlite_types.rb
59
85
  - lib/sqlite_types/array.rb
60
86
  - lib/sqlite_types/interval.rb
61
87
  - lib/sqlite_types/ip_address.rb
88
+ - lib/sqlite_types/migration_helpers.rb
62
89
  - lib/sqlite_types/version.rb
63
90
  - sig/sqlite_types.rbs
64
91
  homepage: https://github.com/wojtodzio/activerecord-sqlite-types
data/.envrc DELETED
@@ -1,5 +0,0 @@
1
- export DIRENV_WARN_TIMEOUT=20s
2
-
3
- eval "$(devenv direnvrc)"
4
-
5
- use devenv
data/.standard.yml DELETED
@@ -1,3 +0,0 @@
1
- # For available configuration options, see:
2
- # https://github.com/standardrb/standard
3
- ruby_version: 3.1
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- Minitest::TestTask.create
7
-
8
- require "standard/rake"
9
-
10
- task default: %i[test standard]
data/devenv.lock DELETED
@@ -1,171 +0,0 @@
1
- {
2
- "nodes": {
3
- "devenv": {
4
- "locked": {
5
- "dir": "src/modules",
6
- "lastModified": 1761156818,
7
- "owner": "cachix",
8
- "repo": "devenv",
9
- "rev": "949fc6dc8f36f38e1cceb1bf1673c4e995a6a766",
10
- "type": "github"
11
- },
12
- "original": {
13
- "dir": "src/modules",
14
- "owner": "cachix",
15
- "repo": "devenv",
16
- "type": "github"
17
- }
18
- },
19
- "flake-compat": {
20
- "flake": false,
21
- "locked": {
22
- "lastModified": 1747046372,
23
- "owner": "edolstra",
24
- "repo": "flake-compat",
25
- "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
26
- "type": "github"
27
- },
28
- "original": {
29
- "owner": "edolstra",
30
- "repo": "flake-compat",
31
- "type": "github"
32
- }
33
- },
34
- "flake-compat_2": {
35
- "flake": false,
36
- "locked": {
37
- "lastModified": 1747046372,
38
- "owner": "edolstra",
39
- "repo": "flake-compat",
40
- "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
41
- "type": "github"
42
- },
43
- "original": {
44
- "owner": "edolstra",
45
- "repo": "flake-compat",
46
- "type": "github"
47
- }
48
- },
49
- "flake-utils": {
50
- "inputs": {
51
- "systems": "systems"
52
- },
53
- "locked": {
54
- "lastModified": 1731533236,
55
- "owner": "numtide",
56
- "repo": "flake-utils",
57
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
58
- "type": "github"
59
- },
60
- "original": {
61
- "owner": "numtide",
62
- "repo": "flake-utils",
63
- "type": "github"
64
- }
65
- },
66
- "git-hooks": {
67
- "inputs": {
68
- "flake-compat": "flake-compat",
69
- "gitignore": "gitignore",
70
- "nixpkgs": [
71
- "nixpkgs"
72
- ]
73
- },
74
- "locked": {
75
- "lastModified": 1760663237,
76
- "owner": "cachix",
77
- "repo": "git-hooks.nix",
78
- "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
79
- "type": "github"
80
- },
81
- "original": {
82
- "owner": "cachix",
83
- "repo": "git-hooks.nix",
84
- "type": "github"
85
- }
86
- },
87
- "gitignore": {
88
- "inputs": {
89
- "nixpkgs": [
90
- "git-hooks",
91
- "nixpkgs"
92
- ]
93
- },
94
- "locked": {
95
- "lastModified": 1709087332,
96
- "owner": "hercules-ci",
97
- "repo": "gitignore.nix",
98
- "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
99
- "type": "github"
100
- },
101
- "original": {
102
- "owner": "hercules-ci",
103
- "repo": "gitignore.nix",
104
- "type": "github"
105
- }
106
- },
107
- "nixpkgs": {
108
- "locked": {
109
- "lastModified": 1758532697,
110
- "owner": "cachix",
111
- "repo": "devenv-nixpkgs",
112
- "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
113
- "type": "github"
114
- },
115
- "original": {
116
- "owner": "cachix",
117
- "ref": "rolling",
118
- "repo": "devenv-nixpkgs",
119
- "type": "github"
120
- }
121
- },
122
- "nixpkgs-ruby": {
123
- "inputs": {
124
- "flake-compat": "flake-compat_2",
125
- "flake-utils": "flake-utils",
126
- "nixpkgs": [
127
- "nixpkgs"
128
- ]
129
- },
130
- "locked": {
131
- "lastModified": 1759902829,
132
- "owner": "bobvanderlinden",
133
- "repo": "nixpkgs-ruby",
134
- "rev": "5fba6c022a63f1e76dee4da71edddad8959f088a",
135
- "type": "github"
136
- },
137
- "original": {
138
- "owner": "bobvanderlinden",
139
- "repo": "nixpkgs-ruby",
140
- "type": "github"
141
- }
142
- },
143
- "root": {
144
- "inputs": {
145
- "devenv": "devenv",
146
- "git-hooks": "git-hooks",
147
- "nixpkgs": "nixpkgs",
148
- "nixpkgs-ruby": "nixpkgs-ruby",
149
- "pre-commit-hooks": [
150
- "git-hooks"
151
- ]
152
- }
153
- },
154
- "systems": {
155
- "locked": {
156
- "lastModified": 1681028828,
157
- "owner": "nix-systems",
158
- "repo": "default",
159
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
160
- "type": "github"
161
- },
162
- "original": {
163
- "owner": "nix-systems",
164
- "repo": "default",
165
- "type": "github"
166
- }
167
- }
168
- },
169
- "root": "root",
170
- "version": 7
171
- }
data/devenv.nix DELETED
@@ -1,14 +0,0 @@
1
- { pkgs, ... }:
2
-
3
- {
4
- languages = {
5
- ruby = {
6
- version = "3.1";
7
- enable = true;
8
- };
9
- };
10
-
11
- packages = with pkgs; [
12
- (sqlite.override { interactive = true; })
13
- ];
14
- }
data/devenv.yaml DELETED
@@ -1,8 +0,0 @@
1
- inputs:
2
- nixpkgs:
3
- url: github:cachix/devenv-nixpkgs/rolling
4
- nixpkgs-ruby:
5
- url: github:bobvanderlinden/nixpkgs-ruby
6
- inputs:
7
- nixpkgs:
8
- follows: nixpkgs