├── .github └── workflows │ └── main.yml ├── .gitignore ├── .standard.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE-DEPENDENCIES.txt ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── builds └── sqlean-linux-x86-musl.zip ├── lib ├── sqlean.rb └── sqlean │ ├── upstream.rb │ └── version.rb ├── rakelib ├── package.rake ├── standard.rake └── test.rake ├── sqlean.gemspec └── test ├── test_helper.rb └── test_sqlean.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | concurrency: 4 | group: "${{ github.workflow }}-${{ github.ref }}" 5 | cancel-in-progress: true 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3 10 | push: 11 | branches: 12 | - main 13 | tags: 14 | - v*.*.* 15 | pull_request: 16 | types: [opened, synchronize] 17 | branches: 18 | - '*' 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - run: rm -f Gemfile.lock 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: "3.3" 29 | bundler-cache: true 30 | - run: bundle exec rake standard 31 | 32 | basic: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - run: rm -f Gemfile.lock 37 | - uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: "3.3" 40 | bundler-cache: true 41 | - run: bundle exec rake test 42 | 43 | non-linux: 44 | name: "${{ matrix.os }} ${{ matrix.ruby }}" 45 | needs: basic 46 | runs-on: ${{ matrix.os }} 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: 51 | - macos-13 # x86_64-darwin 52 | - macos-14 # arm64-darwin 53 | - windows-latest # x64-mingw32 and x64-mingw-ucrt 54 | ruby: ["3.1", "3.2", "3.3"] 55 | steps: 56 | - uses: actions/checkout@v4 57 | - run: rm -f Gemfile.lock 58 | shell: bash 59 | - uses: ruby/setup-ruby@v1 60 | with: 61 | ruby-version: ${{ matrix.ruby }} 62 | bundler-cache: true 63 | - run: bundle exec rake test 64 | 65 | linux: 66 | name: "${{ matrix.platform }} ${{ matrix.ruby }}" 67 | needs: basic 68 | runs-on: ubuntu-latest 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | platform: 73 | - aarch64-linux-gnu 74 | # - aarch64-linux-musl # builds not available yet 75 | - x86_64-linux-gnu 76 | - x86_64-linux-musl 77 | ruby: ["3.1", "3.2", "3.3"] 78 | include: 79 | - { platform: x86_64-linux-musl, docker_tag: "-alpine" } 80 | # - { platform: aarch64-linux-musl, docker_tag: "-alpine", docker_platform: "--platform=linux/arm64" } 81 | - { platform: aarch64-linux-gnu, docker_platform: "--platform=linux/arm64" } 82 | steps: 83 | - uses: actions/checkout@v4 84 | - run: rm -f Gemfile.lock 85 | - run: | 86 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 87 | docker run --rm -v $PWD:/work -w /work \ 88 | ${{ matrix.docker_platform }} ruby:${{ matrix.ruby }}${{ matrix.docker_tag }} \ 89 | sh -c "bundle install --without=lint && bundle exec rake test" 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /archive/ 5 | /coverage/ 6 | /doc/ 7 | /lib/sqlean/dist 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/standardrb/standard 3 | ruby_version: 3.0 4 | ignore: 5 | - "pkg/**/*" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SQLean for Ruby Changelog 2 | 3 | ## v0.3.0 / 2024-11-26 4 | 5 | ### Breaking change 6 | 7 | - Drop `#sqlite_extension_path` for the simpler `#to_path`. [#3] @flavorjones 8 | 9 | 10 | ## v0.2.0 / 2024-11-24 11 | 12 | - Support for x86_64-linux-musl with self-built extensions. 13 | - Explicitly state no-support for aarch64-linux-musl. 14 | 15 | 16 | ## v0.1.0 / 2024-11-24 17 | 18 | - Initial experimental release 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in sqlean.gemspec 6 | gemspec 7 | 8 | group :development do 9 | gem "rake" 10 | gem "rubyzip" 11 | end 12 | 13 | group :lint do 14 | gem "standard" 15 | end 16 | 17 | group :test do 18 | gem "minitest" 19 | gem "sqlite3" 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | sqlean (0.3.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | json (2.8.2) 11 | language_server-protocol (3.17.0.3) 12 | lint_roller (1.1.0) 13 | mini_portile2 (2.8.8) 14 | minitest (5.25.2) 15 | parallel (1.26.3) 16 | parser (3.3.6.0) 17 | ast (~> 2.4.1) 18 | racc 19 | racc (1.8.1) 20 | rainbow (3.1.1) 21 | rake (13.2.1) 22 | regexp_parser (2.9.2) 23 | rubocop (1.68.0) 24 | json (~> 2.3) 25 | language_server-protocol (>= 3.17.0) 26 | parallel (~> 1.10) 27 | parser (>= 3.3.0.2) 28 | rainbow (>= 2.2.2, < 4.0) 29 | regexp_parser (>= 2.4, < 3.0) 30 | rubocop-ast (>= 1.32.2, < 2.0) 31 | ruby-progressbar (~> 1.7) 32 | unicode-display_width (>= 2.4.0, < 3.0) 33 | rubocop-ast (1.36.1) 34 | parser (>= 3.3.1.0) 35 | rubocop-performance (1.22.1) 36 | rubocop (>= 1.48.1, < 2.0) 37 | rubocop-ast (>= 1.31.1, < 2.0) 38 | ruby-progressbar (1.13.0) 39 | rubyzip (2.3.2) 40 | sqlite3 (2.3.1) 41 | mini_portile2 (~> 2.8.0) 42 | sqlite3 (2.3.1-x86_64-linux-gnu) 43 | standard (1.42.1) 44 | language_server-protocol (~> 3.17.0.2) 45 | lint_roller (~> 1.0) 46 | rubocop (~> 1.68.0) 47 | standard-custom (~> 1.0.0) 48 | standard-performance (~> 1.5) 49 | standard-custom (1.0.2) 50 | lint_roller (~> 1.0) 51 | rubocop (~> 1.50) 52 | standard-performance (1.5.0) 53 | lint_roller (~> 1.1) 54 | rubocop-performance (~> 1.22.0) 55 | unicode-display_width (2.6.0) 56 | 57 | PLATFORMS 58 | ruby 59 | x86_64-linux 60 | 61 | DEPENDENCIES 62 | minitest 63 | rake 64 | rubyzip 65 | sqlean! 66 | sqlite3 67 | standard 68 | 69 | BUNDLED WITH 70 | 2.5.23 71 | -------------------------------------------------------------------------------- /LICENSE-DEPENDENCIES.txt: -------------------------------------------------------------------------------- 1 | The SQLean ruby gem ships with a third party dependency, SQLean, whose license is included below. 2 | 3 | The original license can be found at https://github.com/nalgeon/sqlean/blob/main/LICENSE 4 | 5 | MIT License 6 | 7 | Copyright (c) 2021+ Anton Zhiyanov 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Mike Dalessio 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLean Ruby 2 | 3 | Precompiled [SQLean](https://github.com/nalgeon/sqlean) extensions for SQLite, packaged for the Ruby ecosystem. Compatible with SQLite3, Extralite, and any other sqlite-based library that exposes [`sqlite3_load_extension`](https://www.sqlite.org/c3ref/load_extension.html). 4 | 5 | 6 | ## Usage 7 | 8 | Please read the upstream documentation at https://antonz.org/sqlean/ or https://github.com/nalgeon/sqlean for details on what is provided by the SQLean extensions. 9 | 10 | The available extensions are: 11 | 12 | - [SQLean::Crypto](https://github.com/nalgeon/sqlean/blob/main/docs/crypto.md): Hashing, encoding and decoding data 13 | - [SQLean::Define](https://github.com/nalgeon/sqlean/blob/main/docs/define.md): User-defined functions and dynamic SQL 14 | - [SQLean::FileIO](https://github.com/nalgeon/sqlean/blob/main/docs/fileio.md): Reading and writing files 15 | - [SQLean::Fuzzy](https://github.com/nalgeon/sqlean/blob/main/docs/fuzzy.md): Fuzzy string matching and phonetics 16 | - [SQLean::IPAddr](https://github.com/nalgeon/sqlean/blob/main/docs/ipaddr.md): IP address manipulation (not supported on Windows) 17 | - [SQLean::Math](https://github.com/nalgeon/sqlean/blob/main/docs/math.md): Math functions 18 | - [SQLean::Regexp](https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md): Regular expressions 19 | - [SQLean::Stats](https://github.com/nalgeon/sqlean/blob/main/docs/stats.md): Math statistics 20 | - [SQLean::Text](https://github.com/nalgeon/sqlean/blob/main/docs/text.md): String functions 21 | - [SQLean::Unicode](https://github.com/nalgeon/sqlean/blob/main/docs/unicode.md): Unicode support 22 | - [SQLean::UUID](https://github.com/nalgeon/sqlean/blob/main/docs/uuid.md): Universally Unique IDentifiers 23 | - [SQLean::VSV](https://github.com/nalgeon/sqlean/blob/main/docs/vsv.md): CSV files as virtual tables 24 | 25 | 26 | ### with SQLite3 27 | 28 | Extend a SQLite3 database with SQLean extensions: 29 | 30 | ``` ruby 31 | require "sqlite3" 32 | require "sqlean" 33 | 34 | db = SQLite3::Database.new("path/to/db.sqlite") 35 | db.enable_load_extension(true) 36 | 37 | db.load_extension(SQLean.to_path) # load every extension in SQLean 38 | db.load_extension(SQLean::Crypto.to_path) # or load individual extensions 39 | ``` 40 | 41 | 49 | 50 | 65 | 66 | ### with Extralite 67 | 68 | Extend an Extralite database with SQLean extensions: 69 | 70 | ``` ruby 71 | require "extralite" 72 | require "sqlean" 73 | 74 | db = Extralite::Database.new("path/to/db.sqlite") 75 | 76 | db.load_extension(SQLean.to_path) # load every extension in SQLean 77 | db.load_extension(SQLean::Crypto.to_path) # or load individual extensions 78 | ``` 79 | 80 | 81 | ## Installation 82 | 83 | Install the gem and add to the application's Gemfile by executing: 84 | 85 | ```bash 86 | bundle add sqlean 87 | ``` 88 | 89 | If bundler is not being used to manage dependencies, install the gem by executing: 90 | 91 | ```bash 92 | gem install sqlean 93 | ``` 94 | 95 | Note that right now, the only platforms supported are: 96 | 97 | - MacOS / Darwin: 98 | - x86_64 99 | - arm64 100 | - Linux 101 | - x86_64 gnu 102 | - x86_64 musl 103 | - aarch64 gnu 104 | - Windows 105 | - mingw (64-bit) 106 | 107 | Specifically what's missing is support for: 108 | 109 | - Linux aarch64 musl 110 | - Windows mingw32 (32-bit) 111 | 112 | If you need support for one of these platforms, please open an issue. I would also gladly welcome folks who are willing to help add support. 113 | 114 | 115 | ## Development 116 | 117 | 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. 118 | 119 | 120 | ## Contributing 121 | 122 | Bug reports and pull requests are welcome on GitHub at https://github.com/flavorjones/sqlean-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/flavorjones/sqlean-ruby/blob/main/CODE_OF_CONDUCT.md). 123 | 124 | 125 | ## License 126 | 127 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 128 | 129 | 130 | ## Code of Conduct 131 | 132 | Everyone interacting in the sqlean-ruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/flavorjones/sqlean-ruby/blob/main/CODE_OF_CONDUCT.md). 133 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # intentionally left blank. see rakelib/ 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "sqlean" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /builds/sqlean-linux-x86-musl.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flavorjones/sqlean-ruby/dbcfca3db7c2c100f5b1dcfb80c6faf46ea0325d/builds/sqlean-linux-x86-musl.zip -------------------------------------------------------------------------------- /lib/sqlean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "sqlean/version" 4 | require_relative "sqlean/upstream" 5 | 6 | # https://github.com/nalgeon/sqlean/blob/main/README.md 7 | module SQLean 8 | class UnsupportedPlatform < StandardError; end 9 | 10 | GEM_NAME = "sqlean" 11 | 12 | # Returns an absolute path to the SQLean bundle, containing all the SQLean extensions. 13 | def self.to_path 14 | SQLean.file_path("sqlean") 15 | end 16 | 17 | # https://github.com/nalgeon/sqlean/blob/main/docs/crypto.md 18 | module Crypto 19 | # Returns an absolute path to the SQLean crypto extension. 20 | def self.to_path 21 | SQLean.file_path("crypto") 22 | end 23 | end 24 | 25 | # https://github.com/nalgeon/sqlean/blob/main/docs/define.md 26 | module Define 27 | # Returns an absolute path to the SQLean define extension. 28 | def self.to_path 29 | SQLean.file_path("define") 30 | end 31 | end 32 | 33 | # https://github.com/nalgeon/sqlean/blob/main/docs/fileio.md 34 | module FileIO 35 | # Returns an absolute path to the SQLean fileio extension. 36 | def self.to_path 37 | SQLean.file_path("fileio") 38 | end 39 | end 40 | 41 | # https://github.com/nalgeon/sqlean/blob/main/docs/fuzzy.md 42 | module Fuzzy 43 | # Returns an absolute path to the SQLean fuzzy extension. 44 | def self.to_path 45 | SQLean.file_path("fuzzy") 46 | end 47 | end 48 | 49 | # https://github.com/nalgeon/sqlean/blob/main/docs/ipaddr.md 50 | module IPAddr 51 | # Returns an absolute path to the SQLean ipaddr extension. 52 | def self.to_path 53 | SQLean.file_path("ipaddr") 54 | end 55 | end 56 | 57 | # https://github.com/nalgeon/sqlean/blob/main/docs/math.md 58 | module Math 59 | # Returns an absolute path to the SQLean math extension. 60 | def self.to_path 61 | SQLean.file_path("math") 62 | end 63 | end 64 | 65 | # https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md 66 | module Regexp 67 | # Returns an absolute path to the SQLean regexp extension. 68 | def self.to_path 69 | SQLean.file_path("regexp") 70 | end 71 | end 72 | 73 | # https://github.com/nalgeon/sqlean/blob/main/docs/stats.md 74 | module Stats 75 | # Returns an absolute path to the SQLean stats extension. 76 | def self.to_path 77 | SQLean.file_path("stats") 78 | end 79 | end 80 | 81 | # https://github.com/nalgeon/sqlean/blob/main/docs/text.md 82 | module Text 83 | # Returns an absolute path to the SQLean text extension. 84 | def self.to_path 85 | SQLean.file_path("text") 86 | end 87 | end 88 | 89 | # https://github.com/nalgeon/sqlean/blob/main/docs/time.md 90 | module Time 91 | # Returns an absolute path to the SQLean text extension. 92 | def self.to_path 93 | SQLean.file_path("time") 94 | end 95 | end 96 | 97 | # https://github.com/nalgeon/sqlean/blob/main/docs/unicode.md 98 | module Unicode 99 | # Returns an absolute path to the SQLean unicode extension. 100 | def self.to_path 101 | SQLean.file_path("unicode") 102 | end 103 | end 104 | 105 | # https://github.com/nalgeon/sqlean/blob/main/docs/uuid.md 106 | module UUID 107 | # Returns an absolute path to the SQLean uuid extension. 108 | def self.to_path 109 | SQLean.file_path("uuid") 110 | end 111 | end 112 | 113 | # https://github.com/nalgeon/sqlean/blob/main/docs/vsv.md 114 | module VSV 115 | # Returns an absolute path to the SQLean vsv extension. 116 | def self.to_path 117 | SQLean.file_path("vsv") 118 | end 119 | end 120 | 121 | # 122 | # "private" methods 123 | # 124 | def self.file_path(name) # :nodoc: 125 | File.join(SQLean.file_dir, name) 126 | end 127 | 128 | def self.file_dir # :nodoc: 129 | @file_arch ||= begin 130 | check_arch 131 | 132 | Dir.glob(File.join(__dir__, "sqlean", "dist", "*")).find do |f| 133 | Gem::Platform.match_gem?(Gem::Platform.new(File.basename(f)), GEM_NAME) 134 | end 135 | end 136 | end 137 | 138 | def self.check_arch # :nodoc: 139 | if SQLean::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match_gem?(Gem::Platform.new(p), GEM_NAME) } 140 | raise UnsupportedPlatform, "#{GEM_NAME} does not support the #{platform} platform." 141 | end 142 | end 143 | 144 | # here mostly for testing purposes (to stub) 145 | def self.platform # :nodoc: 146 | RUBY_PLATFORM 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/sqlean/upstream.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SQLean 4 | module Upstream 5 | # The version of upstream sqlean extensions used. 6 | VERSION = "0.27.1" 7 | 8 | # rubygems platform name => upstream release filename fragment 9 | NATIVE_PLATFORMS = { 10 | "aarch64-linux-gnu" => "linux-arm64", 11 | "x86_64-linux-gnu" => "linux-x86", 12 | "x86_64-linux-musl" => "linux-x86-musl", 13 | 14 | "arm64-darwin" => "macos-arm64", 15 | "x86_64-darwin" => "macos-x86", 16 | 17 | "x64-mingw" => "win-x64" 18 | } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/sqlean/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SQLean 4 | # The version of the SQLean gem. 5 | VERSION = "0.3.0" 6 | end 7 | -------------------------------------------------------------------------------- /rakelib/package.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # Rake tasks to manage native gem packages with precompiled extensions from https://github.com/nalgeon/sqlean 5 | # 6 | # TL;DR: run "rake package" 7 | # 8 | # The native platform gems (defined by SQLean::Upstream::NATIVE_PLATFORMS) will each contain 9 | # a set of precompiled sqlite extension files: 10 | # 11 | # lib/ 12 | # └─ sqlean/ 13 | # └─ #{Gem::Platform architecture name}/ 14 | # ├─ crypto.#{DLEXT} 15 | # ├─ uuid.#{DLEXT} 16 | # └─ ... 17 | # 18 | # As a concrete example, an x86_64-linux system will see these files on disk after installation: 19 | # 20 | # lib/ 21 | # └─ sqlean/ 22 | # └─ x86_64-linux/ 23 | # ├─ crypto.so 24 | # ├─ uuid.so 25 | # └─ ... 26 | # 27 | # So the full set of gem files created will be: 28 | # 29 | # - pkg/sqlean-1.0.0-x86_64-linux-gnu.gem 30 | # - pkg/sqlean-1.0.0-x86_64-linux-musl.gem 31 | # - pkg/sqlean-1.0.0-aarch64-linux-gnu.gem 32 | # - pkg/sqlean-1.0.0-arm64-darwin.gem 33 | # - pkg/sqlean-1.0.0-x86_64-darwin.gem 34 | # - pkg/sqlean-1.0.0-x64-mingw.gem 35 | # 36 | # Note that we do not ship a vanilla "ruby" gem, or an aarch64-linux gem, or a mingw32 gem. 37 | # 38 | # Note that upstream does not provide musl builds, but I did build the x86_64-linux-musl version 39 | # and checked it into the builds/ directory. However, I'm too lazy right now to build an 40 | # aarch64-linux-musl version. 41 | # 42 | # 43 | # New rake tasks created: 44 | # 45 | # - rake package # Build all the gem files 46 | # - rake repackage # Force a rebuild of all the gem files 47 | # - rake gem:aarch64-linux # Build the aarch64-linux gem 48 | # - rake gem:x86_64-linux # Build the x86_64-linux gem 49 | # - rake gem:arm64-darwin # Build the arm64-darwin gem 50 | # - rake gem:x86_64-darwin # Build the x86_64-darwin gem 51 | # - rake gem:x64-mingw # Build the x64-mingw gem 52 | # - rake download # Download all sqlean extension files 53 | # 54 | require_relative "../lib/sqlean/upstream" 55 | 56 | require "open-uri" 57 | require "rake/clean" 58 | require "rubygems/package_task" 59 | require "zip" 60 | 61 | # 62 | # downloading and unzipping 63 | # 64 | def sqlean_download_url(filename) 65 | "https://github.com/nalgeon/sqlean/releases/download/#{SQLean::Upstream::VERSION}/sqlean-#{filename}.zip" 66 | end 67 | 68 | archive = "archive" 69 | directory archive 70 | CLOBBER.add archive 71 | 72 | dist = "lib/sqlean/dist" 73 | directory dist 74 | CLOBBER.add dist 75 | 76 | # nodoc 77 | task "unzip" 78 | 79 | desc "Download and unzip all the precompiled sqlean extensions" 80 | task "download" => "unzip" 81 | 82 | SQLean::Upstream::NATIVE_PLATFORMS.each do |platform, filename| 83 | release_url = sqlean_download_url(filename) 84 | zip_path = File.join(archive, "sqlean-#{filename}.zip") 85 | build_path = File.join("builds", "sqlean-#{filename}.zip") 86 | 87 | install_path = File.join(dist, platform) 88 | directory install_path 89 | 90 | file zip_path => archive do 91 | if File.exist?(build_path) 92 | warn "Using prebuilt #{build_path} ..." 93 | FileUtils.cp(build_path, zip_path) 94 | else 95 | warn "Downloading #{zip_path} from #{release_url} ..." 96 | OpenURI.open_uri(release_url) do |remote| 97 | File.binwrite(zip_path, remote.read) 98 | end 99 | end 100 | end 101 | 102 | task "unzip:#{platform}" => [zip_path, install_path] do 103 | Zip::File.open(zip_path) do |zip| 104 | warn "Unzipping #{zip_path} ..." 105 | zip.each do |file| 106 | new_path = File.join(install_path, file.name) 107 | # puts "extracting: #{file.name.inspect} → #{new_path.inspect} ..." 108 | 109 | FileUtils.rm(new_path) if File.exist?(new_path) 110 | 111 | file.restore_permissions = true 112 | file.extract(new_path) 113 | end 114 | end 115 | end 116 | 117 | desc "Download and unzip extensions for #{platform}" 118 | task "download:#{platform}" => "unzip:#{platform}" 119 | 120 | task "download" => "download:#{platform}" 121 | task "unzip" => "unzip:#{platform}" 122 | end 123 | 124 | GEM_PLATFORM_LOCAL = Gem::Platform.local.then do |p| 125 | version = if p.os == "linux" 126 | p.version || "gnu" 127 | end 128 | [p.cpu, p.os, version].compact.join("-") 129 | end 130 | 131 | desc "Download and unzip extensions for the current platform (#{GEM_PLATFORM_LOCAL})" 132 | task "download:local" => "download:#{GEM_PLATFORM_LOCAL}" 133 | 134 | # 135 | # packaging 136 | # 137 | SQLEAN_GEMSPEC = Bundler.load_gemspec("sqlean.gemspec") 138 | 139 | desc "Build all the gem files" 140 | task "package" => "download" 141 | 142 | desc "Rebuild all the gem files" 143 | task "repackage" => ["clobber", "package"] 144 | 145 | SQLean::Upstream::NATIVE_PLATFORMS.each do |platform, filename| 146 | desc "Build the #{platform} gem" 147 | task "gem:#{platform}" => "download:#{platform}" do 148 | gemspec = SQLEAN_GEMSPEC.dup 149 | gemspec.platform = platform 150 | gemspec.files += Dir.glob(File.join(dist, platform, "*")) 151 | 152 | gem_task = Gem::PackageTask.new(gemspec).define 153 | gem_task.invoke 154 | end 155 | 156 | task "package" => "gem:#{platform}" 157 | end 158 | 159 | CLOBBER.add("pkg") 160 | -------------------------------------------------------------------------------- /rakelib/standard.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "standard/rake" 5 | task "default" => "standard" 6 | rescue LoadError 7 | warn "NOTE: Standard is not loaded in this environment." 8 | end 9 | -------------------------------------------------------------------------------- /rakelib/test.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/test_task" 4 | 5 | Minitest::TestTask.create 6 | 7 | task "test" => "download:local" 8 | task "default" => "test" 9 | -------------------------------------------------------------------------------- /sqlean.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/sqlean/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "sqlean" 7 | spec.version = SQLean::VERSION 8 | spec.authors = ["Mike Dalessio"] 9 | spec.email = ["mike.dalessio@gmail.com"] 10 | 11 | spec.summary = "Precompiled SQLean extensions for SQLite, packaged for the Ruby ecosystem." 12 | spec.description = <<~TEXT 13 | Precompiled SQLean extensions for SQLite, packaged for the Ruby ecosystem. Compatible with 14 | SQLite3, Extralite, and any other sqlite-based library that exposes sqlite3_load_extension. 15 | TEXT 16 | 17 | spec.license = "MIT" 18 | spec.required_ruby_version = ">= 3.0.0" 19 | 20 | spec.homepage = "https://github.com/flavorjones/sqlean-ruby" 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | spec.metadata["changelog_uri"] = spec.homepage + "/blob/main/CHANGELOG.md" 24 | 25 | spec.metadata["rubygems_mfa_required"] = "true" 26 | 27 | # note that the extension files are injected by the rake task in rakelib/package.rake 28 | spec.files = [ 29 | "CHANGELOG.md", 30 | "LICENSE.txt", 31 | "LICENSE-DEPENDENCIES.txt", 32 | "README.md", 33 | "lib/sqlean.rb", 34 | "lib/sqlean/version.rb", 35 | "lib/sqlean/upstream.rb" 36 | ] 37 | 38 | spec.require_paths = ["lib"] 39 | end 40 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/sqlean" 4 | require "sqlite3" 5 | require "minitest/autorun" 6 | -------------------------------------------------------------------------------- /test/test_sqlean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TestSQLean < Minitest::Spec 6 | # 7 | # shared specs, one for each extension 8 | # 9 | def self.it_extends_crypto 10 | it "extends crypto" do 11 | result = db.execute("select hex(crypto_blake3('abc'));") 12 | 13 | assert_equal([["6437B3AC38465133FFB63B75273A8DB548C558465D79DB03FD359C6CD5BD9D85"]], result) 14 | end 15 | end 16 | 17 | def self.it_extends_define 18 | it "extends define" do 19 | db.execute("select define('sumn', ':n * (:n + 1) / 2');") 20 | result = db.execute("select sumn(5);") 21 | 22 | assert_equal([[15]], result) 23 | end 24 | end 25 | 26 | def self.it_extends_fileio 27 | it "extends fileio" do 28 | Tempfile.create do |file| 29 | file.write("Hello, world!") 30 | file.close 31 | 32 | result = db.execute("select fileio_read('#{file.path}');") 33 | 34 | assert_equal([["Hello, world!"]], result) 35 | end 36 | end 37 | end 38 | 39 | def self.it_extends_fuzzy 40 | it "extends fuzzy" do 41 | result = db.execute("select fuzzy_damlev('awesome', 'aewsme');") 42 | 43 | assert_equal([[2]], result) 44 | end 45 | end 46 | 47 | def self.it_extends_ipaddr 48 | it "extends ipaddr" do 49 | skip("not supported on windows") if Gem.win_platform? 50 | 51 | result = db.execute("select ipcontains('192.168.16.0/24', '192.168.16.3');") 52 | 53 | assert_equal([[1]], result) 54 | end 55 | end 56 | 57 | def self.it_extends_math 58 | it "extends math" do 59 | result = db.execute("select math_sqrt(9);") 60 | 61 | assert_equal([[3]], result) 62 | end 63 | end 64 | 65 | def self.it_extends_regexp 66 | it "extends regexp" do 67 | result = db.execute("select true where 'the year is 2021' regexp '[0-9]+';") 68 | 69 | assert_equal([[1]], result) 70 | end 71 | end 72 | 73 | def self.it_extends_stats 74 | it "extends stats" do 75 | result = db.execute("select * from stats_seq(1, 4);") 76 | 77 | assert_equal([[1], [2], [3], [4]], result) 78 | end 79 | end 80 | 81 | def self.it_extends_text 82 | it "extends text" do 83 | result = db.execute("select text_substring('hello world', 7);") 84 | 85 | assert_equal([["world"]], result) 86 | end 87 | end 88 | 89 | def self.it_extends_time 90 | it "extends time" do 91 | result = db.execute("select time_fmt_iso(time_date(2011, 11, 18));") 92 | 93 | assert_equal([["2011-11-18T00:00:00Z"]], result) 94 | end 95 | end 96 | 97 | def self.it_extends_uuid 98 | it "extends uuid" do 99 | result = db.execute("select uuid7_timestamp_ms('018ff38a-a5c9-712d-bc80-0550b3ad41a2');") 100 | 101 | assert_equal([[1717777901001]], result) 102 | end 103 | end 104 | 105 | def self.it_extends_vsv 106 | it "extends vsv" do 107 | Tempfile.create do |file| 108 | file.puts("color,size,amount") 109 | file.puts("red,large,10") 110 | file.puts("blue,small,20") 111 | file.puts("green,medium,15") 112 | file.close 113 | 114 | db.execute(<<~SQL) 115 | create virtual table shirts using vsv( 116 | filename='#{file.path}', 117 | schema='create table shirts(color string, size string, amount integer)', 118 | columns=3, 119 | affinity=integer 120 | ); 121 | SQL 122 | db.results_as_hash = true 123 | 124 | result = db.execute("select * from shirts where (color = 'red');") 125 | 126 | assert_equal([{"color" => "red", "size" => "large", "amount" => 10}], result) 127 | end 128 | end 129 | end 130 | 131 | # 132 | # the actual tests 133 | # 134 | let(:db) do 135 | SQLite3::Database.new(":memory:").tap do |db| 136 | db.enable_load_extension(true) 137 | if SQLite3::VERSION < "2.4" || rand(2).zero? # cover both paths 138 | db.load_extension(extension.to_path) 139 | else 140 | db.load_extension(extension) 141 | end 142 | end 143 | end 144 | 145 | describe SQLean do 146 | let(:extension) { SQLean } 147 | 148 | it_extends_crypto 149 | it_extends_define 150 | it_extends_fileio 151 | it_extends_fuzzy 152 | it_extends_ipaddr 153 | it_extends_math 154 | it_extends_regexp 155 | it_extends_stats 156 | it_extends_text 157 | it_extends_time 158 | it_extends_uuid 159 | it_extends_vsv 160 | end 161 | 162 | describe SQLean::Crypto do 163 | let(:extension) { SQLean::Crypto } 164 | 165 | it_extends_crypto 166 | end 167 | 168 | describe SQLean::Define do 169 | let(:extension) { SQLean::Define } 170 | 171 | it_extends_define 172 | end 173 | 174 | describe SQLean::FileIO do 175 | let(:extension) { SQLean::FileIO } 176 | 177 | it_extends_fileio 178 | end 179 | 180 | describe SQLean::Fuzzy do 181 | let(:extension) { SQLean::Fuzzy } 182 | 183 | it_extends_fuzzy 184 | end 185 | 186 | describe SQLean::IPAddr do 187 | let(:extension) { SQLean::IPAddr } 188 | 189 | it_extends_ipaddr 190 | end 191 | 192 | describe SQLean::Math do 193 | let(:extension) { SQLean::Math } 194 | 195 | it_extends_math 196 | end 197 | 198 | describe SQLean::Regexp do 199 | let(:extension) { SQLean::Regexp } 200 | 201 | it_extends_regexp 202 | end 203 | 204 | describe SQLean::Stats do 205 | let(:extension) { SQLean::Stats } 206 | 207 | it_extends_stats 208 | end 209 | 210 | describe SQLean::Text do 211 | let(:extension) { SQLean::Text } 212 | 213 | it_extends_text 214 | end 215 | 216 | describe SQLean::Time do 217 | let(:extension) { SQLean::Time } 218 | 219 | it_extends_time 220 | end 221 | 222 | describe SQLean::UUID do 223 | let(:extension) { SQLean::UUID } 224 | 225 | it_extends_uuid 226 | end 227 | 228 | describe SQLean::VSV do 229 | let(:extension) { SQLean::VSV } 230 | 231 | it_extends_vsv 232 | end 233 | end 234 | --------------------------------------------------------------------------------