relationizer 0.3.1 → 0.4.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: f70691fd83bd02baeab60d894732c1bb51fa2cc9b5f324efa631e873b82acbcf
4
- data.tar.gz: ae5f6b5a66a89bb6ee8bd7204a17253ea406827bc60987767b8f3afa76211758
3
+ metadata.gz: a48df925940ca5fe199ef9ba19ed66836a55ec029356c80dd93e9f4a2f150558
4
+ data.tar.gz: 4add1225e3324e97e22e2670628820adf5c1588cd764e303c6ed4a765130d72c
5
5
  SHA512:
6
- metadata.gz: fbc366ffee800c89f2d16a691d75b8ee2103d5807b5da4eab487bb35b39ca9874c7dbd3e401fc5d525d7da9042d5568e03a3e90056cade18fc6dbad4e3ddbd43
7
- data.tar.gz: def44734c1abe5dad8a9387c237723aeb6d7a8c15a60eac7d99c6a4f5d3e1dd9735db8cd381155ed13b5624483dc8d3f86033535219568a4cfbd34e52252d39f
6
+ metadata.gz: f4a6b8ddaa7d11192b39f940a9ae8da1b9eb75d2b71671c616032cd16db730ce8769d1ceaa8092dac62d16e1d003333d2dfb6c2580cfaa692cd263abe1877906
7
+ data.tar.gz: fef4f070c8d10430a531fd67d700279511f33505f8df3c0149d0afff9463857229a872e4ab756370e831c42cdcfd8d64e697bfbbdee18f634252d671353de8e5
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ contents: write
14
+ id-token: write # Required for Trusted Publisher
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: '3.3'
23
+ bundler-cache: true
24
+
25
+ - name: Build gem
26
+ run: gem build relationizer.gemspec
27
+
28
+ - name: Push to RubyGems (Trusted Publisher)
29
+ uses: rubygems/release-gem@v1
30
+
31
+ - name: Create GitHub Release
32
+ uses: softprops/action-gh-release@v1
33
+ with:
34
+ files: relationizer-*.gem
35
+ generate_release_notes: true
@@ -1,20 +1,22 @@
1
1
  name: Ruby
2
2
 
3
- on: [push]
3
+ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
- build:
7
-
6
+ test:
8
7
  runs-on: ubuntu-latest
9
8
 
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ ruby-version: ['3.2', '3.3', '3.4', '4.0']
13
+
10
14
  steps:
11
- - uses: actions/checkout@v1
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
15
+ - uses: actions/checkout@v4
16
+ - name: Set up Ruby ${{ matrix.ruby-version }}
17
+ uses: ruby/setup-ruby@v1
14
18
  with:
15
- ruby-version: 2.6.x
16
- - name: Build and test with Rake
17
- run: |
18
- gem install bundler
19
- bundle install --jobs 4 --retry 3
20
- bundle exec rake
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+ bundler-cache: true
21
+ - name: Run tests
22
+ run: bundle exec rake
data/CLAUDE.md ADDED
@@ -0,0 +1,48 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What is Relationizer
6
+
7
+ Ruby gem that converts `Array<Array>` into SQL relation literals. Two backends:
8
+ - **`Relationizer::BigQuery`** → `SELECT * FROM UNNEST(ARRAY<STRUCT<...>>[...])`
9
+ - **`Relationizer::Postgresql`** → `SELECT "col"::TYPE FROM (VALUES(...)) AS t("col")`
10
+
11
+ Both are modules intended to be `include`d. The public API is `create_relation_literal(schema, tuples)`.
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ bundle install # Install dependencies
17
+ bundle exec rake # Run all tests (default task)
18
+ bundle exec rake test # Run all tests (explicit)
19
+ bin/console # IRB with gem loaded
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ lib/relationizer.rb # Namespace module, requires submodules
26
+ lib/relationizer/version.rb # VERSION constant
27
+ lib/relationizer/big_query.rb # BigQuery SQL generation (module)
28
+ lib/relationizer/postgresql.rb # PostgreSQL SQL generation (module)
29
+ ```
30
+
31
+ Each backend module:
32
+ - Infers SQL types from Ruby objects (Integer→INT64/INT8, String→STRING/TEXT, etc.)
33
+ - Supports manual type specification via schema hash values
34
+ - Has custom error classes (`ReasonlessTypeError`, `TypeNotFoundError`)
35
+ - Handles edge cases: NULL, single-column relations (BigQuery adds dummy column), empty tuples, Infinity
36
+
37
+ ## Testing
38
+
39
+ - Framework: **test-unit** (~> 3.0.0)
40
+ - Test files: `test/*_test.rb`
41
+ - Helper: `test/to_one_line.rb` — refinement that normalizes multi-line SQL for assertion comparison
42
+ - Tests use `data` method for parameterized test cases
43
+
44
+ ## Notes
45
+
46
+ - Zero runtime dependencies (pure Ruby)
47
+ - Column names are quoted (`"col"` for PG, `` `col` `` for BQ)
48
+ - String escaping differs per backend (doubled single quotes for PG, backslash escaping for BQ)
data/README.md CHANGED
@@ -1,41 +1,143 @@
1
1
  # Relationizer
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/relationizer`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ A Ruby gem that converts `Array<Array>` into SQL relation literals. Supports BigQuery and PostgreSQL.
6
4
 
7
5
  ## Installation
8
6
 
9
- Add this line to your application's Gemfile:
10
-
11
7
  ```ruby
12
8
  gem 'relationizer'
13
9
  ```
14
10
 
15
- And then execute:
11
+ ```
12
+ $ bundle install
13
+ ```
14
+
15
+ ## Usage
16
16
 
17
- $ bundle
17
+ `include` the backend module you need and call `create_relation_literal(schema, tuples)`.
18
18
 
19
- Or install it yourself as:
19
+ - `schema` A Hash of column names to types. Set the value to `nil` to auto-infer the type from the tuples.
20
+ - `tuples` — Row data as `Array<Array>`.
20
21
 
21
- $ gem install relationizer
22
+ ### BigQuery
22
23
 
23
- ## Usage
24
+ ```ruby
25
+ require 'relationizer/big_query'
24
26
 
25
- TODO: Write usage instructions here
27
+ class MyQuery
28
+ include Relationizer::BigQuery
29
+ end
26
30
 
27
- ## Development
31
+ q = MyQuery.new
28
32
 
29
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
33
+ q.create_relation_literal(
34
+ { id: nil, name: nil },
35
+ [[1, 'hoge'], [2, 'fuga']]
36
+ )
37
+ #=> "SELECT * FROM UNNEST(ARRAY<STRUCT<`id` INT64, `name` STRING>>[(1, 'hoge'), (2, 'fuga')])"
38
+ ```
30
39
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
+ #### Auto type inference
32
41
 
33
- ## Contributing
42
+ | Ruby type | BigQuery type |
43
+ |-----------------------|---------------|
44
+ | `Integer` | INT64 |
45
+ | `Float` / `BigDecimal` | FLOAT64 |
46
+ | `String` | STRING |
47
+ | `TrueClass` / `FalseClass` | BOOL |
48
+ | `Time` / `DateTime` | TIMESTAMP |
49
+ | `Date` | DATE |
50
+ | `Array` | ARRAY<T> |
34
51
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/relationizer.
52
+ #### Manual type specification
36
53
 
54
+ Pass a Symbol as the schema value to override auto-inference. Useful when a column contains mixed types or when tuples are empty.
37
55
 
38
- ## License
56
+ ```ruby
57
+ # Force ratio column to FLOAT64 (mixed Integer and Float)
58
+ q.create_relation_literal(
59
+ { id: nil, ratio: :FLOAT64 },
60
+ [[1, 1], [2, 3.14]]
61
+ )
62
+ #=> "SELECT * FROM UNNEST(ARRAY<STRUCT<`id` INT64, `ratio` FLOAT64>>[(1, 1), (2, 3.14)])"
63
+
64
+ # Empty tuples (manual type specification is required)
65
+ q.create_relation_literal(
66
+ { id: :INT64, name: :STRING },
67
+ []
68
+ )
69
+ #=> "SELECT * FROM UNNEST(ARRAY<STRUCT<`id` INT64, `name` STRING>>[])"
70
+ ```
71
+
72
+ #### Array columns
73
+
74
+ ```ruby
75
+ q.create_relation_literal(
76
+ { id: nil, name: nil, combination: nil },
77
+ [[1, 'hoge', [1, 2, 3]], [2, 'fuga', [4, 5, 6]]]
78
+ )
79
+ #=> "SELECT * FROM UNNEST(ARRAY<STRUCT<`id` INT64, `name` STRING, `combination` ARRAY<INT64>>>[(1, 'hoge', [1, 2, 3]), (2, 'fuga', [4, 5, 6])])"
80
+ ```
81
+
82
+ #### Single column
83
+
84
+ BigQuery does not support single-column STRUCTs in UNNEST, so a dummy column is added internally. Only the original column is returned in the SELECT.
85
+
86
+ ```ruby
87
+ q.create_relation_literal(
88
+ { id: nil },
89
+ [[1], [2], [3]]
90
+ )
91
+ #=> "SELECT id FROM UNNEST(ARRAY<STRUCT<`id` INT64, `___dummy` STRING>>[(1, NULL), (2, NULL), (3, NULL)])"
92
+ ```
93
+
94
+ ### PostgreSQL
95
+
96
+ ```ruby
97
+ require 'relationizer/postgresql'
98
+
99
+ class MyQuery
100
+ include Relationizer::Postgresql
101
+ end
102
+
103
+ q = MyQuery.new
104
+
105
+ q.create_relation_literal(
106
+ { id: nil, name: nil },
107
+ [[1, 'hoge'], [2, 'fuga']]
108
+ )
109
+ #=> %Q{SELECT "id"::INT8, "name"::TEXT FROM (VALUES('1', 'hoge'), ('2', 'fuga')) AS t("id", "name")}
110
+ ```
111
+
112
+ #### Auto type inference
39
113
 
40
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
114
+ | Ruby type | PostgreSQL type |
115
+ |-----------------------|-----------------|
116
+ | `Integer` | INT8 |
117
+ | `Float` | FLOAT8 |
118
+ | `BigDecimal` | DECIMAL |
119
+ | `String` | TEXT |
120
+ | `TrueClass` / `FalseClass` | BOOLEAN |
121
+ | `Time` / `DateTime` | TIMESTAMPTZ |
122
+ | `Date` | DATE |
123
+
124
+ #### NULL
125
+
126
+ `nil` values are converted to SQL `NULL`.
127
+
128
+ ```ruby
129
+ q.create_relation_literal(
130
+ { id: nil },
131
+ [[1], [nil]]
132
+ )
133
+ #=> %Q{SELECT "id"::INT8 FROM (VALUES('1'), (NULL)) AS t("id")}
134
+ ```
135
+
136
+ ## Errors
137
+
138
+ - `ReasonlessTypeError` — Raised when types are mixed within a single column (e.g. Integer and String in the same column)
139
+ - `TypeNotFoundError` (BigQuery only) — Raised when tuples are empty and types are not manually specified
140
+
141
+ ## License
41
142
 
143
+ [MIT License](http://opensource.org/licenses/MIT)
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+ require 'bigdecimal'
3
+ require 'date'
4
+
5
+ module Relationizer
6
+ module MySQL
7
+ class ReasonlessTypeError < StandardError; end
8
+ class TypeNotFoundError < StandardError; end
9
+
10
+ DEFAULT_TYPES = -> (obj) {
11
+ case obj
12
+ when Integer then :BIGINT
13
+ when BigDecimal then :'DECIMAL(65,30)'
14
+ when Float then :DOUBLE
15
+ when String then :TEXT
16
+ when TrueClass then :BOOLEAN
17
+ when FalseClass then :BOOLEAN
18
+ when Time then :DATETIME
19
+ when DateTime then :DATETIME
20
+ when Date then :DATE
21
+ else
22
+ nil
23
+ end
24
+ }
25
+
26
+ def create_relation_literal(schema, tuples)
27
+ types = fixed_types(schema.values, tuples)
28
+ names = schema.keys
29
+
30
+ json_string = to_json_document(names, types, tuples)
31
+ escaped = escape_for_sql(json_string)
32
+ columns_clause = to_columns_clause(names, types)
33
+
34
+ %[(SELECT * FROM JSON_TABLE('#{escaped}', "$[*]" COLUMNS(#{columns_clause})) AS t)]
35
+ end
36
+
37
+ private
38
+
39
+ def fixed_types(schema_values, tuples)
40
+ if tuples.empty?
41
+ raise TypeNotFoundError unless schema_values.all?
42
+ return schema_values
43
+ end
44
+
45
+ tuples.transpose.zip(schema_values).map { |values, type|
46
+ next type if type
47
+
48
+ values.
49
+ map(&DEFAULT_TYPES).compact.uniq.
50
+ tap(&method(:empty_candidate_check)).
51
+ tap(&method(:many_candidate_check)).
52
+ first
53
+ }
54
+ end
55
+
56
+ def many_candidate_check(types)
57
+ raise ReasonlessTypeError.new("Many candidate: #{types.join(', ')}") unless types.one?
58
+ end
59
+
60
+ def empty_candidate_check(types)
61
+ raise ReasonlessTypeError.new("Candidate nothing") if types.empty?
62
+ end
63
+
64
+ def to_json_value(obj, type)
65
+ return nil if obj.nil?
66
+
67
+ case type
68
+ when :'DECIMAL(65,30)'
69
+ obj.is_a?(BigDecimal) ? obj.to_s("F") : obj.to_s
70
+ when :DATE
71
+ obj.strftime('%Y-%m-%d')
72
+ when :DATETIME
73
+ obj.strftime('%Y-%m-%d %H:%M:%S')
74
+ else
75
+ obj
76
+ end
77
+ end
78
+
79
+ def to_json_document(names, types, tuples)
80
+ rows = tuples.map { |tuple|
81
+ hash = {}
82
+ names.zip(tuple, types).each do |name, value, type|
83
+ hash[name] = to_json_value(value, type)
84
+ end
85
+ hash
86
+ }
87
+ JSON.generate(rows)
88
+ end
89
+
90
+ def escape_for_sql(json_string)
91
+ json_string.gsub('\\', '\\\\\\\\').gsub("'", "''")
92
+ end
93
+
94
+ def identifier_quote(name)
95
+ "`#{name}`"
96
+ end
97
+
98
+ def to_columns_clause(names, types)
99
+ names.zip(types).map { |name, type|
100
+ %[#{identifier_quote(name)} #{type} PATH "$.#{name}"]
101
+ }.join(', ')
102
+ end
103
+ end
104
+ end
@@ -1,3 +1,3 @@
1
1
  module Relationizer
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/relationizer.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative "relationizer/version"
2
2
  require_relative "relationizer/postgresql"
3
3
  require_relative "relationizer/big_query"
4
+ require_relative "relationizer/mysql"
4
5
 
5
6
  module Relationizer
6
7
  end
data/relationizer.gemspec CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "bigdecimal"
22
+
21
23
  spec.add_development_dependency "bundler"
22
24
  spec.add_development_dependency "rake", ">= 12.3.3"
23
- spec.add_development_dependency "test-unit", "~> 3.0.0"
25
+ spec.add_development_dependency "test-unit", "~> 3.0"
24
26
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relationizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yancya
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-17 00:00:00.000000000 Z
11
+ date: 2026-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,23 +58,25 @@ dependencies:
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 3.0.0
61
+ version: '3.0'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: 3.0.0
55
- description:
68
+ version: '3.0'
69
+ description:
56
70
  email:
57
71
  - yancya@upec.jp
58
72
  executables: []
59
73
  extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
76
+ - ".github/workflows/release.yml"
62
77
  - ".github/workflows/ruby.yml"
63
78
  - ".gitignore"
79
+ - CLAUDE.md
64
80
  - Gemfile
65
81
  - LICENSE.txt
66
82
  - README.md
@@ -69,6 +85,7 @@ files:
69
85
  - bin/setup
70
86
  - lib/relationizer.rb
71
87
  - lib/relationizer/big_query.rb
88
+ - lib/relationizer/mysql.rb
72
89
  - lib/relationizer/postgresql.rb
73
90
  - lib/relationizer/version.rb
74
91
  - relationizer.gemspec
@@ -76,7 +93,7 @@ homepage: https://github.com/yancya/relationizer
76
93
  licenses:
77
94
  - MIT
78
95
  metadata: {}
79
- post_install_message:
96
+ post_install_message:
80
97
  rdoc_options: []
81
98
  require_paths:
82
99
  - lib
@@ -91,8 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
108
  - !ruby/object:Gem::Version
92
109
  version: '0'
93
110
  requirements: []
94
- rubygems_version: 3.0.3
95
- signing_key:
111
+ rubygems_version: 3.5.22
112
+ signing_key:
96
113
  specification_version: 4
97
114
  summary: Array<Array> to Relation ppoi String
98
115
  test_files: []