snowflaked 0.1.0-x86_64-linux

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f68c35e56f2b711babd4f66a36c759964a5d62cfd43e3076032491ef08af8f0a
4
+ data.tar.gz: ac779781bedeb1379d52e10ba6d86f72dad333b7d9ea424d99b711fa27bf302f
5
+ SHA512:
6
+ metadata.gz: 4d7cd9826ae0ec4bbf15fb591d29c831e06678f82739b5b4e351540c5e69e4f45b7f1f179f131436e01200bd72320119536681e5a5abdeddd872a48771d95e7f
7
+ data.tar.gz: f47b9bbfab1f8a4e259d4cafcb1669fba31a6a16a758a2e53e6178d6cfef16ac6afa2325a0c05af4ed1ebaa9ff251373f9649684bd9163ef2bd0f7e709711051
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Luiz Eduardo Kowalski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Snowflaked
2
+
3
+ A high-performance, thread-safe Snowflake ID generator for Ruby, powered by Rust.
4
+
5
+ Snowflake IDs are 64-bit unique identifiers that encode a timestamp, machine ID, and sequence number. They're time-sortable (IDs created later are always larger), making them ideal for distributed systems where you need unique IDs without coordination between machines. Unlike UUIDs, Snowflake IDs are smaller, sortable, and index-friendly for databases.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "snowflaked"
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```ruby
18
+ id = Snowflaked.id
19
+ # => 7193489234823847936
20
+ ```
21
+
22
+ ## Rails Integration
23
+
24
+ All models automatically generate a Snowflake ID for the `:id` attribute:
25
+
26
+ ```ruby
27
+ class User < ApplicationRecord
28
+ end
29
+
30
+ User.create!
31
+ # => #<User id: 7193489234823847936>
32
+ ```
33
+
34
+ You can also define additional Snowflake columns in migrations:
35
+
36
+ ```ruby
37
+ class CreateUsers < ActiveRecord::Migration[8.1]
38
+ def change
39
+ create_table :users do |t|
40
+ t.snowflake :external_id
41
+ end
42
+ end
43
+ end
44
+ ```
45
+
46
+ If you want to generate Snowflake IDs for additional columns, you can do so by using the `snowflake_id` method, without having to migrate the table:
47
+
48
+ ```ruby
49
+ class User < ApplicationRecord
50
+ snowflake_id :external_id
51
+ end
52
+ ```
53
+
54
+ It is also possible to disable automatic `:id` generation by passing `id: false` to the `snowflake_id` method:
55
+
56
+ ```ruby
57
+ class Post < ApplicationRecord
58
+ snowflake_id id: false
59
+ end
60
+ ```
61
+
62
+ Or generate Snowflake IDs for other columns but not `:id`:
63
+
64
+ ```ruby
65
+ class Post < ApplicationRecord
66
+ snowflake_id :external_id, id: false
67
+ end
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ ```ruby
73
+ Snowflaked.configure do |config|
74
+ config.machine_id = 42
75
+ config.epoch = Time.utc(2020, 1, 1)
76
+ end
77
+ ```
78
+
79
+ ### Machine ID Resolution
80
+
81
+ If `machine_id` is not explicitly configured, it resolves in this order:
82
+
83
+ 1. `SNOWFLAKED_MACHINE_ID` environment variable
84
+ 2. `MACHINE_ID` environment variable
85
+ 3. Auto-detected using the following formula: `(hostname.hash ^ pid) % 1024`
86
+
87
+ For Kubernetes deployments, you can set the machine ID using an environment variable:
88
+
89
+ ```yaml
90
+ env:
91
+ - name: SNOWFLAKED_MACHINE_ID
92
+ valueFrom:
93
+ fieldRef:
94
+ fieldPath: metadata.name
95
+ ```
96
+
97
+ Or use a StatefulSet ordinal for guaranteed unique values:
98
+
99
+ ```yaml
100
+ env:
101
+ - name: SNOWFLAKED_MACHINE_ID
102
+ valueFrom:
103
+ fieldRef:
104
+ fieldPath: metadata.annotations['apps.kubernetes.io/pod-index']
105
+ ```
106
+
107
+ ## API Reference
108
+
109
+ ```ruby
110
+ id = Snowflaked.id
111
+
112
+ Snowflaked.parse(id)
113
+ # => {timestamp_ms: 1735123456789, machine_id: 42, sequence: 0}
114
+
115
+ Snowflaked.timestamp(id)
116
+ # => 2024-12-25 12:34:56 +0000
117
+
118
+ Snowflaked.timestamp_ms(id)
119
+ # => 1735123456789
120
+
121
+ Snowflaked.sequence(id)
122
+ # => 0
123
+
124
+ Snowflaked.machine_id(id)
125
+ # => 42
126
+ ```
127
+
128
+ ## Requirements
129
+
130
+ - Ruby >= 3.2
131
+ - rustc >= 1.81.0
132
+ - cargo >= 1.81.0
133
+ - Mise
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ mise install
139
+ bundle install
140
+ bundle exec rake
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowflaked
4
+ module AdapterExtension
5
+ SNOWFLAKE_TYPE = { name: "bigint" }.freeze
6
+
7
+ def native_database_types
8
+ super.merge(snowflake: SNOWFLAKE_TYPE)
9
+ end
10
+
11
+ module ClassMethods
12
+ def native_database_types
13
+ super.merge(snowflake: SNOWFLAKE_TYPE)
14
+ end
15
+ end
16
+
17
+ def self.prepended(base)
18
+ base.singleton_class.prepend(ClassMethods)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowflaked
4
+ module ModelExtensions
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :_snowflake_attributes, instance_writer: false, default: [:id] # rubocop:disable ThreadSafety/ClassAndModuleAttributes -- false positive
9
+ before_validation :_generate_snowflake_ids, on: :create
10
+ end
11
+
12
+ class_methods do
13
+ def snowflake_id(*attributes, id: true)
14
+ attrs = attributes.map(&:to_sym)
15
+ attrs |= [:id] if id
16
+ self._snowflake_attributes = attrs
17
+ end
18
+
19
+ def _snowflake_columns_from_comments
20
+ return [] unless table_exists?
21
+
22
+ columns.select { |c| c.comment == Snowflaked::SchemaDefinitions::COMMENT }.map { |c| c.name.to_sym }
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def _generate_snowflake_ids
29
+ attributes_to_generate = self.class._snowflake_attributes | self.class._snowflake_columns_from_comments
30
+
31
+ attributes_to_generate.each do |attribute|
32
+ next if self[attribute].present?
33
+
34
+ self[attribute] = Snowflaked.id
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Snowflaked
6
+ class Railtie < Rails::Railtie
7
+ initializer "snowflaked.register_type", after: "active_record.initialize_database" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ require "snowflaked/type/snowflake"
10
+ ActiveRecord::Type.register(:snowflake, Snowflaked::Type::Snowflake)
11
+ end
12
+ end
13
+
14
+ initializer "snowflaked.extend_adapters", after: "active_record.initialize_database" do
15
+ ActiveSupport.on_load(:active_record) do
16
+ require "snowflaked/adapter_extension"
17
+
18
+ adapters = %w[
19
+ PostgreSQLAdapter
20
+ Mysql2Adapter
21
+ TrilogyAdapter
22
+ SQLite3Adapter
23
+ ]
24
+
25
+ adapters.each do |adapter_name|
26
+ next unless ActiveRecord::ConnectionAdapters.const_defined?(adapter_name)
27
+
28
+ ActiveRecord::ConnectionAdapters.const_get(adapter_name).prepend(
29
+ Snowflaked::AdapterExtension
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ initializer "snowflaked.schema_definitions", after: "active_record.initialize_database" do
36
+ ActiveSupport.on_load(:active_record) do
37
+ require "snowflaked/schema_definitions"
38
+
39
+ ActiveRecord::ConnectionAdapters::TableDefinition.include(Snowflaked::SchemaDefinitions::TableDefinition)
40
+ ActiveRecord::ConnectionAdapters::Table.include(Snowflaked::SchemaDefinitions::Table)
41
+ end
42
+ end
43
+
44
+ initializer "snowflaked.model_extensions", after: "active_record.initialize_database" do
45
+ ActiveSupport.on_load(:active_record) do
46
+ require "snowflaked/model_extensions"
47
+ ActiveRecord::Base.include(Snowflaked::ModelExtensions)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowflaked
4
+ module SchemaDefinitions
5
+ COMMENT = "snowflaked"
6
+
7
+ module TableDefinition
8
+ def snowflake(name, **options)
9
+ options[:comment] = Snowflaked::SchemaDefinitions::COMMENT
10
+ column(name, :snowflake, **options)
11
+ end
12
+ end
13
+
14
+ module Table
15
+ def snowflake(name, **options)
16
+ options[:comment] = Snowflaked::SchemaDefinitions::COMMENT
17
+ column(name, :snowflake, **options)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowflaked
4
+ module Type
5
+ class Snowflake < ActiveRecord::Type::BigInteger
6
+ def type
7
+ :snowflake
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowflaked
4
+ VERSION = "0.1.0"
5
+ end
data/lib/snowflaked.rb ADDED
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "snowflaked/version"
4
+ require_relative "snowflaked/snowflaked"
5
+ require "socket"
6
+
7
+ require_relative "snowflaked/railtie" if defined?(Rails::Railtie)
8
+
9
+ module Snowflaked
10
+ MAX_MACHINE_ID = 1023
11
+
12
+ class Error < StandardError; end
13
+ class ConfigurationError < Error; end
14
+
15
+ class Configuration
16
+ attr_accessor :machine_id, :epoch
17
+
18
+ def initialize
19
+ @machine_id = nil
20
+ @epoch = nil
21
+ end
22
+
23
+ def machine_id_value
24
+ id = @machine_id || default_machine_id
25
+ id % (MAX_MACHINE_ID + 1)
26
+ end
27
+
28
+ def epoch_ms
29
+ return nil unless @epoch
30
+
31
+ (@epoch.to_f * 1000).to_i
32
+ end
33
+
34
+ private
35
+
36
+ def default_machine_id
37
+ env_machine_id || hostname_pid_hash
38
+ end
39
+
40
+ def env_machine_id
41
+ id = ENV["SNOWFLAKED_MACHINE_ID"] || ENV.fetch("MACHINE_ID", nil)
42
+ id&.to_i
43
+ end
44
+
45
+ def hostname_pid_hash
46
+ (Socket.gethostname.hash ^ Process.pid) % (MAX_MACHINE_ID + 1)
47
+ end
48
+ end
49
+
50
+ class << self
51
+ def configuration
52
+ @configuration ||= Configuration.new # rubocop:disable ThreadSafety/ClassInstanceVariable -- not changed after initialization
53
+ end
54
+
55
+ def configure
56
+ yield(configuration) if block_given?
57
+
58
+ ensure_initialized!
59
+ configuration
60
+ end
61
+
62
+ def id
63
+ ensure_initialized!
64
+ Native.generate
65
+ end
66
+
67
+ def parse(id)
68
+ ensure_initialized!
69
+ Native.parse(id)
70
+ end
71
+
72
+ def timestamp(id)
73
+ time_ms = Native.timestamp_ms(id)
74
+ Time.at(time_ms / 1000, (time_ms % 1000) * 1000, :usec)
75
+ end
76
+
77
+ def machine_id(id)
78
+ ensure_initialized!
79
+ Native.machine_id(id)
80
+ end
81
+
82
+ def timestamp_ms(id)
83
+ Native.timestamp_ms(id)
84
+ end
85
+
86
+ def sequence(id)
87
+ Native.sequence(id)
88
+ end
89
+
90
+ private
91
+
92
+ def ensure_initialized!
93
+ return if Native.initialized?
94
+
95
+ config = configuration
96
+ Native.init_generator(config.machine_id_value, config.epoch_ms)
97
+ end
98
+ end
99
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snowflaked
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x86_64-linux
6
+ authors:
7
+ - Luiz Eduardo Kowalski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-01-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A Ruby gem for generating Twitter Snowflake IDs using a high-performance
14
+ Rust backend. Thread-safe with configurable machine ID and custom epoch support.
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - lib/snowflaked.rb
23
+ - lib/snowflaked/3.2/snowflaked.so
24
+ - lib/snowflaked/3.3/snowflaked.so
25
+ - lib/snowflaked/3.4/snowflaked.so
26
+ - lib/snowflaked/4.0/snowflaked.so
27
+ - lib/snowflaked/adapter_extension.rb
28
+ - lib/snowflaked/model_extensions.rb
29
+ - lib/snowflaked/railtie.rb
30
+ - lib/snowflaked/schema_definitions.rb
31
+ - lib/snowflaked/type/snowflake.rb
32
+ - lib/snowflaked/version.rb
33
+ homepage: https://github.com/luizkowalski/snowflaked
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/luizkowalski/snowflaked
38
+ source_code_uri: https://github.com/luizkowalski/snowflaked
39
+ changelog_uri: https://github.com/luizkowalski/snowflaked/blob/main/CHANGES.md
40
+ bug_tracker_uri: https://github.com/luizkowalski/snowflaked/issues
41
+ rubygems_mfa_required: 'true'
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '3.2'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: 4.1.dev
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.23
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Fast Snowflake ID generator with Rust backend
64
+ test_files: []