├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .iex.exs ├── CHANGELOG.md ├── LICENSE ├── README.md ├── all_checks.sh ├── config └── config.exs ├── coveralls.json ├── lib ├── ets.ex └── ets │ ├── bag.ex │ ├── base.ex │ ├── key_value_set.ex │ ├── key_value_set │ └── macros.ex │ ├── set.ex │ └── utils.ex ├── mix.exs ├── mix.lock ├── plts └── ignore └── test ├── ets ├── bag_test.exs ├── set │ └── key_value_set_test.exs └── set_test.exs ├── ets_test.exs ├── support ├── test_server.ex └── test_utils.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: true, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterFilter, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 137 | 138 | # 139 | ## Warnings 140 | # 141 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 142 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 143 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 144 | {Credo.Check.Warning.IExPry, []}, 145 | {Credo.Check.Warning.IoInspect, []}, 146 | {Credo.Check.Warning.OperationOnSameValues, []}, 147 | {Credo.Check.Warning.OperationWithConstantResult, []}, 148 | {Credo.Check.Warning.RaiseInsideRescue, []}, 149 | {Credo.Check.Warning.SpecWithStruct, []}, 150 | {Credo.Check.Warning.WrongTestFileExtension, []}, 151 | {Credo.Check.Warning.UnusedEnumOperation, []}, 152 | {Credo.Check.Warning.UnusedFileOperation, []}, 153 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 154 | {Credo.Check.Warning.UnusedListOperation, []}, 155 | {Credo.Check.Warning.UnusedPathOperation, []}, 156 | {Credo.Check.Warning.UnusedRegexOperation, []}, 157 | {Credo.Check.Warning.UnusedStringOperation, []}, 158 | {Credo.Check.Warning.UnusedTupleOperation, []}, 159 | {Credo.Check.Warning.UnsafeExec, []} 160 | ], 161 | disabled: [ 162 | # 163 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 164 | 165 | # 166 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 167 | # and be sure to use `mix credo --strict` to see low priority checks) 168 | # 169 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 170 | {Credo.Check.Consistency.UnusedVariableNames, []}, 171 | {Credo.Check.Design.DuplicatedCode, []}, 172 | {Credo.Check.Design.SkipTestWithoutComment, []}, 173 | {Credo.Check.Readability.AliasAs, []}, 174 | {Credo.Check.Readability.BlockPipe, []}, 175 | {Credo.Check.Readability.ImplTrue, []}, 176 | {Credo.Check.Readability.MultiAlias, []}, 177 | {Credo.Check.Readability.NestedFunctionCalls, []}, 178 | {Credo.Check.Readability.SeparateAliasRequire, []}, 179 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 180 | {Credo.Check.Readability.SinglePipe, []}, 181 | {Credo.Check.Readability.Specs, []}, 182 | {Credo.Check.Readability.StrictModuleLayout, []}, 183 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 184 | {Credo.Check.Refactor.ABCSize, []}, 185 | {Credo.Check.Refactor.AppendSingleItem, []}, 186 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 187 | {Credo.Check.Refactor.FilterReject, []}, 188 | {Credo.Check.Refactor.IoPuts, []}, 189 | {Credo.Check.Refactor.MapMap, []}, 190 | {Credo.Check.Refactor.ModuleDependencies, []}, 191 | {Credo.Check.Refactor.NegatedIsNil, []}, 192 | {Credo.Check.Refactor.PipeChainStart, []}, 193 | {Credo.Check.Refactor.RejectFilter, []}, 194 | {Credo.Check.Refactor.VariableRebinding, []}, 195 | {Credo.Check.Warning.LazyLogging, []}, 196 | {Credo.Check.Warning.LeakyEnvironment, []}, 197 | {Credo.Check.Warning.MapGetUnsafePass, []}, 198 | {Credo.Check.Warning.MixEnv, []}, 199 | {Credo.Check.Warning.UnsafeToAtom, []} 200 | 201 | # {Credo.Check.Refactor.MapInto, []}, 202 | 203 | # 204 | # Custom checks can be created using `mix credo.gen.check`. 205 | # 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: [master] 8 | env: 9 | MIX_ENV: test 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | name: Test / OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 16 | strategy: 17 | matrix: 18 | otp: ['24.3', '25.0'] 19 | elixir: ['1.13.4'] 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup elixir 24 | uses: erlef/setup-beam@v1 25 | with: 26 | elixir-version: ${{ matrix.elixir }} # Define the elixir version [required] 27 | otp-version: ${{ matrix.otp }} # Define the OTP version [required] 28 | 29 | - name: Retrieve Mix Dependencies Cache 30 | uses: actions/cache@v1 31 | id: mix-deps-cache #id to use in retrieve action 32 | with: 33 | path: deps 34 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-${{ github.event.pull_request.base.sha }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 35 | restore-keys: | 36 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-${{ github.event.pull_request.base.sha }}- 37 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps 38 | 39 | - name: Retrieve Mix Build Cache 40 | uses: actions/cache@v1 41 | id: mix-build-cache #id to use in retrieve action 42 | with: 43 | path: _build/test/lib 44 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-build-${{ github.event.pull_request.base.sha }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 45 | restore-keys: | 46 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-build-${{ github.event.pull_request.base.sha }}- 47 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-build- 48 | 49 | - name: Install Mix Dependencies 50 | #if: steps.mix-deps-cache.outputs.cache-hit != 'true' 51 | run: | 52 | mix local.rebar --force 53 | mix local.hex --force 54 | mix deps.get 55 | 56 | - name: Check Formatting 57 | run: | 58 | mix format --check-formatted 59 | 60 | - name: Clean 61 | run: mix clean 62 | 63 | - name: Compile 64 | run: mix compile 65 | 66 | - name: Run Credo 67 | run: mix credo --strict 68 | 69 | - name: Run Tests 70 | # run: mix test 71 | run: mix coveralls.github 72 | 73 | - name: Retrieve PLT Cache 74 | uses: actions/cache@v1 75 | id: plt-cache 76 | with: 77 | path: plts 78 | key: v5-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ github.event.pull_request.base.sha }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 79 | restore-keys: | 80 | v5-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ github.event.pull_request.base.sha }}- 81 | v5-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts- 82 | 83 | - name: Run dialyzer 84 | run: mix dialyzer 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ets-*.tar 24 | 25 | /.elixir_ls/ 26 | 27 | .DS_Store 28 | 29 | 30 | /plts/*.plt 31 | /plts/*.plt.hash -------------------------------------------------------------------------------- /.iex.exs: -------------------------------------------------------------------------------- 1 | alias ETS.Bag 2 | alias ETS.Base 3 | alias ETS.Set 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.0 4 | 5 | - Add `update_element/3` - Thanks @APB9785 6 | - Add `match_object/{1,2,3}` and `match_delete/2` - Thanks @APB9785 7 | - Add `give_away/3`, `accept/1`, and `accept/6` - Thanks @APB9785 8 | 9 | ## 0.8.1 10 | 11 | - Add Set.fetch/2 - Thanks @christhekeele 12 | 13 | ## 0.8.0 14 | 15 | - Rename `Ets` to `ETS` 16 | - Move `ETS.Set.KeyValueSet` to `ETS.KeyValueSet` 17 | - Add `select/1` and `select/3` to `ETS.set` - Thanks @zachdaniel 18 | - Handle and return :position_out_of_bounds error when calling `get_element`/`lookup_element` with a position greater than the size of one of the returned tuples 19 | - Add documentation for named table usage pattern. 20 | 21 | ## 0.7.3 22 | 23 | - Handle and return :read_protected error when reading from a private table from a different process 24 | 25 | ## 0.7.2 26 | 27 | - Handle and return :write_protected error when inserting into a non-public table from a different process 28 | - Handle and return :invalid_select_spec error 29 | 30 | ## 0.7.1 31 | 32 | - Handle and return :record_too_small when size of inserted record is smaller than keypos 33 | 34 | ## 0.7.0 35 | 36 | - Add `Access` protocol for `KeyValueSet` - Thanks @am-kantox 37 | - Fix return issue in `KeyValueSet` delete/delete_all - Thanks @am-kantox 38 | - Add documentation for choosing which table to use 39 | 40 | ## 0.6.0 41 | 42 | - Add `ETS.KeyValueSet` 43 | 44 | ## 0.5.0 45 | 46 | - Handle and return `:table_already_exists` on `new` 47 | - Fix spec for `Set.get` to reflect possible nil return 48 | - Implemented `delete_all` for `Set` and `Bag` 49 | - Implemented `select` for `Set` and `Bag` 50 | - Implemented `select_delete` for `Set` and `Bag` 51 | - Implemented `get_element`/`lookup_element` for `Set` and `Bag` 52 | - Add `Bag`s to `ETS.all` 53 | - Add list default option values in `new` documentation 54 | 55 | ## 0.4.0 56 | 57 | - Implement `ETS.Bag` 58 | 59 | ## 0.3.0 60 | 61 | - Combined `put_multi` into `put` and `put_multi_new` into `put_new` 62 | - `put_new` with existing key(s) is no longer an error condition 63 | - Catch list of non-tuples passed to `put` or `put_new` 64 | 65 | ## 0.2.2 66 | 67 | - Fix issue with docs 68 | 69 | ## 0.2.1 70 | 71 | - Add `get_table` to access underlying ets table reference (to directly access not-yet-implemented functions of `:ets`) 72 | 73 | ## 0.2.0 74 | 75 | - Redesign from ground up to use module/struct based approach 76 | - Implemented `ETS.Set` and `ETS.Base` 77 | - Set up CI and Readme badges 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mike Binns 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETS 2 | 3 | `:ets`, the Elixir way 4 | 5 | [![Build status](https://github.com/TheFirstAvenger/ets/actions/workflows/ci.yml/badge.svg)](https://github.com/TheFirstAvenger/ets/actions/workflows/ci.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/TheFirstAvenger/ets/badge.svg?branch=master)](https://coveralls.io/github/TheFirstAvenger/ets?branch=master) 7 | [![Project license](https://img.shields.io/hexpm/l/ets.svg)](https://unlicense.org/) 8 | [![Hex.pm package](https://img.shields.io/hexpm/v/ets.svg)](https://hex.pm/packages/ets) 9 | [![Hex.pm downloads](https://img.shields.io/hexpm/dt/ets.svg)](https://hex.pm/packages/ets) 10 | 11 | ETS is a set of Elixir modules that wrap Erlang Term Storage (`:ets`). 12 | 13 | ## Current Features 14 | 15 | - `ETS.Set` - wraps `:set` and `:ordered_set` 16 | - `ETS.Bag` - wraps `:bag` and `:duplicate_bag` 17 | - `ETS.KeyValueSet` - extension of `ETS.Set` that abstracts away tuple and key index concepts into simple key/value inputs/outputs. 18 | - Most used functions from `:ets` replicated for all wrappers 19 | - Returns {:error, reason} tuples (or raises in ! versions) for: 20 | - `:table_not_found` 21 | - `:table_already_exists` 22 | - `:key_not_found` 23 | - `:invalid_record` (when inserting non-tuples) 24 | - `:record_too_small` (tuple smaller than keypos index) 25 | - `:position_out_of_bounds` (`lookup` with a pos > length of one of the results) 26 | - `:invalid_select_spec` 27 | - `:write_protected` - trying to write to a protected or private table from a different process than the owner 28 | - `:read_protected` - trying to read from a private table from a different process than the owner 29 | 30 | ## Design Goals 31 | 32 | The purpose of this package is to improve the developer experience when both learning and interacting with Erlang Term Storage. 33 | 34 | This will be accomplished by: 35 | 36 | - Conforming to Elixir standards: 37 | - Two versions of all functions: 38 | - Main function (e.g. `get`) returns `{:ok, return}`/`{:error, reason}` tuples. 39 | - Bang function (e.g. `get!`) returns unwrapped value or raises on :error. 40 | - All options specified via keyword list. 41 | - Wrapping unhelpful `ArgumentError`'s with appropriate error returns. 42 | - Avoid adding performance overhead by using try/rescue instead of pre-validation 43 | - On rescue, try to determine what went wrong (e.g. missing table) and return appropriate error 44 | - Fall back to `{:error, :unknown_error}` (logging details) if unable to determine reason. 45 | - Appropriate error returns/raises when encountering `$end_of_table`. 46 | - Providing Elixir friendly documentation. 47 | - Providing `ETS.Set` and `ETS.Bag` modules with appropriate function signatures and error handling. 48 | - `ETS.Set.get` returns a single item (or nil/provided default) instead of list as sets never have multiple records for a key. 49 | - Providing abstractions on top of the two base modules for specific usages 50 | - `ETS.KeyValueSet` abstracts away the concept of tuple records, replacing it with standard key/value interactions. 51 | 52 | ## Changes 53 | 54 | For a list of changes, see the [changelog](CHANGELOG.md) 55 | 56 | ## Usage 57 | 58 | ### Creating ETS Tables 59 | 60 | ETS Tables can be created using the `new` function of the appropriate module, either `ETS.Set` 61 | (for ordered and unordered sets) or `ETS.Bag` (for duplicate or non-duplicate bags). 62 | See module documentation for more examples and documentation, including a guide on [What type of ETS table should I use?](lib/ets.ex). 63 | 64 | #### Create Examples 65 | 66 | ```elixir 67 | iex> {:ok, set} = Set.new(ordered: true, keypos: 3, read_concurrency: true, compressed: false) 68 | iex> Set.info!(set)[:read_concurrency] 69 | true 70 | 71 | # Named :ets tables via the name keyword 72 | iex> {:ok, set} = Set.new(name: :my_ets_table) 73 | iex> Set.info!(set)[:name] 74 | :my_ets_table 75 | iex> {:ok, set} = Set.wrap_existing(:my_ets_table) 76 | iex> set = Set.wrap_existing!(:my_ets_table) 77 | ``` 78 | 79 | ### Adding/Updating/Retrieving records in Sets 80 | 81 | To add records to an ETS table, use `put` or `put_new` with a tuple record or a list of tuple records. 82 | `put` will overwrite existing records with the same key. `put_new` not insert if the key 83 | already exists. When passing a list of tuple records, all records are inserted in an atomic and 84 | isolated manner, but with `put_new` no records are inserted if at least one existing key is found. 85 | 86 | #### Set Examples 87 | 88 | ```elixir 89 | iex> set = Set.new!(ordered: true) 90 | iex> |> Set.put!({:a, :b}) 91 | iex> |> Set.put!({:a, :c}) # Overwrites entry from previous line 92 | iex> |> Set.put!({:c, :d}) 93 | iex> Set.get(:a) 94 | {:ok, {:a, :c}} 95 | iex> Set.to_list(set) 96 | {:ok, [{:a, :c}, {:c, :d}]} 97 | 98 | iex> Set.new!(ordered: true) 99 | iex> |> Set.put!({:a, :b}) 100 | iex> |> Set.put_new!({:a, :c}) # Doesn't insert due to key :a already existing 101 | iex> |> Set.to_list!() 102 | [{:a, :b}] 103 | ``` 104 | 105 | #### Bag Examples 106 | 107 | ```elixir 108 | iex> bag = Bag.new!() 109 | iex> |> Bag.add!({:a, :b}) 110 | iex> |> Bag.add!({:a, :c}) 111 | iex> |> Bag.add!({:a, :c}) # Adds dude to duplicate: true 112 | iex> |> Bag.add!({:c, :d}) 113 | iex> Bag.lookup(set, :a) 114 | {:ok, [{:a, :b}, {:a, :c}, {:a, :c}]} 115 | iex> Bag.to_list(bag) 116 | {:ok, [{:a, :b}, {:a, :c}, {:a, :c}, {:c, :d}]} 117 | iex> Bag.add_new!(bag, {:a, :z}) # Doesn't add due to key :a already existing 118 | iex> Bag.to_list(bag) 119 | {:ok, [{:a, :b}, {:a, :c}, {:a, :c}, {:c, :d}]} 120 | 121 | iex> bag = Bag.new!(duplicate: false) 122 | iex> |> Bag.add!({:a, :b}) 123 | iex> |> Bag.add!({:a, :c}) 124 | iex> |> Bag.add!({:a, :c}) # Doesn't add dude to duplicate: false 125 | iex> |> Bag.add!({:c, :d}) 126 | iex> Bag.lookup(bag, :a) 127 | {:ok, [{:a, :b}, {:a, :c}]} 128 | iex> Bag.to_list(bag) 129 | {:ok, [{:a, :b}, {:a, :c}, {:c, :d}]} 130 | ``` 131 | 132 | ## Current Progress 133 | 134 | ### Base Modules 135 | 136 | - [x] `ETS` 137 | - [x] All 138 | - [x] `ETS.Set` 139 | - [x] Put (insert) 140 | - [x] Get (lookup) 141 | - [x] Get Element 142 | - [x] Delete 143 | - [x] Delete All 144 | - [x] First 145 | - [x] Next 146 | - [x] Last 147 | - [x] Previous 148 | - [x] Match 149 | - [x] Select 150 | - [x] Select Delete 151 | - [x] Has Key (Member) 152 | - [x] Info 153 | - [x] Delete 154 | - [x] To List (tab2list) 155 | - [x] Wrap 156 | - [x] `ETS.Bag` 157 | - [x] Add (insert) 158 | - [x] Lookup 159 | - [x] Lookup Element 160 | - [x] Delete 161 | - [x] Delete All 162 | - [x] Match 163 | - [x] Select 164 | - [x] Select Delete 165 | - [x] Has Key (Member) 166 | - [x] Info 167 | - [x] Delete 168 | - [x] To List (tab2list) 169 | - [x] Wrap 170 | 171 | ### Abstractions 172 | 173 | - [x] `ETS.KeyValueSet` 174 | - [x] New 175 | - [x] Wrap Existing 176 | - [x] Put 177 | - [x] Put New 178 | - [x] Get 179 | - [x] Info 180 | - [x] Get Table 181 | - [x] First 182 | - [x] Last 183 | - [x] Next 184 | - [x] Previous 185 | - [x] Has Key 186 | - [x] Delete 187 | - [x] Delete Key 188 | - [x] Delete All 189 | - [x] To List 190 | 191 | ## Installation 192 | 193 | `ETS` can be installed by adding `ets` to your list of dependencies in `mix.exs`: 194 | 195 | ```elixir 196 | def deps do 197 | [ 198 | {:ets, "~> 0.9.0"} 199 | ] 200 | end 201 | ``` 202 | 203 | Docs can be found at . 204 | 205 | ## Contributing 206 | 207 | Contributions welcome. Specifically looking to: 208 | 209 | - Add remainder of functions, ([See Erlang Docs](http://erlang.org/doc/man/ets.html)). 210 | - Discover and add zero-impact recovery for any additional possible `:ets` `ArgumentError`s. 211 | -------------------------------------------------------------------------------- /all_checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf _build/test/lib/ets 4 | MIX_ENV=test mix compile --warnings-as-errors 5 | MIX_ENV=test mix test 6 | MIX_ENV=test mix format --check-formatted 7 | MIX_ENV=test mix credo --strict -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :ets, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:ets, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/ets/key_value_set/macros.ex", 4 | "lib/ets/utils.ex" 5 | ] 6 | } -------------------------------------------------------------------------------- /lib/ets.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS do 2 | use ETS.Utils 3 | 4 | @moduledoc """ 5 | ETS, an Elixir wrapper for Erlang's [`:ets`](http://erlang.org/doc/man/ets.html) module. 6 | 7 | See `ETS.Set` for information on creating and managing Sets, and `ETS.Bag` for information on creating and managing Bags. 8 | 9 | See `ETS.KeyValueSet` for an abstraction which provides standard key/value interaction with Sets. 10 | 11 | ## What type of `ETS` table should I use? 12 | 13 | ## Set 14 | 15 | If you need your key column to be unique, then you should use a Set. If you just want a simple key/value store, 16 | then use an `ETS.KeyValueSet`, but if you want to store full tuple records, use an `ETS.Set`. If you want your 17 | records ordered by key value, which adds some performance overhead on insertion, set `ordered: true` when creating the Set (defaults to false). 18 | 19 | ## Bag 20 | 21 | If you do not need your key column to be unique, then you should use an `ETS.Bag`, and if you want to prevent exact duplicate 22 | records from being inserted, which adds some performance overhead on insertion, set duplicate: false when creating the Bag 23 | (defaults to true). 24 | """ 25 | 26 | @type table_name :: atom() 27 | @type table_reference :: :ets.tid() 28 | @type table_identifier :: table_name | table_reference 29 | @type match_pattern :: :ets.match_pattern() 30 | @type match_spec :: :ets.match_spec() 31 | @type comp_match_spec :: :ets.comp_match_spec() 32 | @type end_of_table :: :"$end_of_table" 33 | @type continuation :: 34 | end_of_table 35 | | {table_reference(), integer(), integer(), comp_match_spec(), list(), integer()} 36 | | {table_reference(), any(), any(), integer(), comp_match_spec(), list(), integer(), 37 | integer()} 38 | 39 | @doc """ 40 | Returns list of current :ets tables, each wrapped as either `ETS.Set` or `ETS.Bag`. 41 | 42 | NOTE: `ETS.Bag` is not yet implemented. This list returns only :set and :ordered_set tables, both wrapped as `ETS.Set`. 43 | 44 | ## Examples 45 | 46 | iex> {:ok, all} = ETS.all() 47 | iex> x = length(all) 48 | iex> ETS.Set.new!() 49 | iex> {:ok, all} = ETS.all() 50 | iex> length(all) == x + 1 51 | true 52 | 53 | """ 54 | @spec all :: {:ok, [ETS.table_identifier()]} | {:error, any()} 55 | def all do 56 | catch_error do 57 | all = 58 | :ets.all() 59 | |> Enum.map(fn tid -> 60 | tid 61 | |> :ets.info() 62 | |> Keyword.get(:type) 63 | |> case do 64 | type when type in [:set, :ordered_set] -> ETS.Set.wrap_existing!(tid) 65 | type when type in [:bag, :duplicate_bag] -> ETS.Bag.wrap_existing!(tid) 66 | end 67 | end) 68 | 69 | {:ok, all} 70 | end 71 | end 72 | 73 | @doc """ 74 | Same as all/1 but unwraps or raises on :error. 75 | 76 | """ 77 | @spec all! :: [ETS.table_identifier()] 78 | def all!, do: unwrap_or_raise(all()) 79 | end 80 | -------------------------------------------------------------------------------- /lib/ets/bag.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.Bag do 2 | @moduledoc """ 3 | Module for creating and interacting with :ets tables of the type `:bag` and `:duplicate_bag`. 4 | 5 | Bags contain "records" which are tuples. Bags are configured with a key position via the `keypos: integer` option. 6 | If not specified, the default key position is 1. The element of the tuple record at the key position is that records key. 7 | For example, setting the `keypos` to 2 means the key of an inserted record `{:a, :b}` is `:b`: 8 | 9 | iex> {:ok, bag} = Bag.new(keypos: 2) 10 | iex> Bag.add!(bag, {:a, :b}) 11 | iex> Bag.lookup(bag, :a) 12 | {:ok, []} 13 | iex> Bag.lookup(bag, :b) 14 | {:ok, [{:a, :b}]} 15 | 16 | When a record is added to the table with `add_new` will only add the record if a matching key doesn't already exist. 17 | 18 | ## Examples 19 | 20 | iex> {:ok, bag} = Bag.new() 21 | iex> Bag.add_new!(bag, {:a, :b, :c}) 22 | iex> Bag.to_list!(bag) 23 | [{:a, :b, :c}] 24 | iex> Bag.add_new!(bag, {:d, :e, :f}) 25 | iex> Bag.to_list!(bag) 26 | [{:d, :e, :f}, {:a, :b, :c}] 27 | iex> Bag.add_new!(bag, {:a, :g, :h}) 28 | iex> Bag.to_list!(bag) 29 | [{:d, :e, :f}, {:a, :b, :c}] 30 | 31 | `add` and `add_new` take either a single tuple or a list of tuple records. When adding multiple records, 32 | they are added in an atomic an isolated manner. `add_new` doesn't add any records if any of 33 | the new keys already exist in the bag. 34 | 35 | By default, Bags allow duplicate records (each element of the tuple record is identical). To prevent duplicate 36 | records, set the `duplicate: false` opt when creating the Bag (if you want to prevent duplicate *keys*, use an `ETS.Set` 37 | instead). Note that `duplicate: false` will increase the time it takes to add records as the table must be checked for 38 | duplicates prior to insert. `duplicate: true` maps to the `:ets` table type `:duplicate_bag`, `duplicate: false` maps to `:bag`. 39 | 40 | ## Working with named tables 41 | 42 | The functions on `ETS.Bag` require that you pass in an `ETS.Bag` as the first argument. In some design patterns, 43 | you may have the table name but an instance of an `ETS.Bag` may not be available to you. If this is the case, 44 | you should use `wrap_existing/1` to turn your table name atom into an `ETS.Bag`. For example, a `GenServer` that 45 | handles writes within the server, but reads in the client process would be implemented like this: 46 | 47 | ``` 48 | defmodule MyExampleGenServer do 49 | use GenServer 50 | alias ETS.Bag 51 | 52 | # Client Functions 53 | 54 | def get_roles_for_user(user_id) do 55 | :my_role_table 56 | |> Bag.wrap_existing!() 57 | |> Bag.lookup!(user_id) 58 | |> Enum.map(&elem(&1, 1)) 59 | end 60 | 61 | def add_role_for_user(user_id, role) do 62 | GenServer.call(__MODULE__, {:add_role_for_user, user_id, role}) 63 | end 64 | 65 | # Server Functions 66 | 67 | def init(_) do 68 | {:ok, %{bag: Bag.new!(name: :my_role_table)}} 69 | end 70 | 71 | def handle_call({:add_role_for_user, user_id, role}, _from, %{bag: bag}) do 72 | Bag.add(bag, {user_id, role}) 73 | end 74 | end 75 | 76 | ``` 77 | 78 | """ 79 | use ETS.Utils 80 | 81 | alias ETS.Bag 82 | alias ETS.Base 83 | 84 | @type t :: %__MODULE__{ 85 | info: keyword(), 86 | duplicate: boolean(), 87 | table: ETS.table_reference() 88 | } 89 | 90 | @type bag_options :: [ETS.Base.option() | {:duplicate, boolean()}] 91 | 92 | defstruct table: nil, info: nil, duplicate: nil 93 | 94 | @doc """ 95 | Creates new bag module with the specified options. 96 | 97 | Note that the underlying :ets table will be attached to the process that calls `new` and will be destroyed 98 | if that process dies. 99 | 100 | Possible options: 101 | 102 | * `name:` when specified, creates a named table with the specified name 103 | * `duplicate:` when true, allows multiple identical records. (default true) 104 | * `protection:` :private, :protected, :public (default :protected) 105 | * `heir:` :none | {heir_pid, heir_data} (default :none) 106 | * `keypos:` integer (default 1) 107 | * `read_concurrency:` boolean (default false) 108 | * `write_concurrency:` boolean (default false) 109 | * `compressed:` boolean (default false) 110 | 111 | ## Examples 112 | 113 | iex> {:ok, bag} = Bag.new(duplicate: false, keypos: 3, read_concurrency: true, compressed: false) 114 | iex> Bag.info!(bag)[:read_concurrency] 115 | true 116 | 117 | # Named :ets tables via the name keyword 118 | iex> {:ok, bag} = Bag.new(name: :my_ets_table) 119 | iex> Bag.info!(bag)[:name] 120 | :my_ets_table 121 | 122 | """ 123 | @spec new(bag_options) :: {:error, any()} | {:ok, Bag.t()} 124 | def new(opts \\ []) when is_list(opts) do 125 | {opts, duplicate} = take_opt(opts, :duplicate, true) 126 | 127 | if is_boolean(duplicate) do 128 | case Base.new_table(type(duplicate), opts) do 129 | {:error, reason} -> {:error, reason} 130 | {:ok, {table, info}} -> {:ok, %Bag{table: table, info: info, duplicate: duplicate}} 131 | end 132 | else 133 | {:error, {:invalid_option, {:duplicate, duplicate}}} 134 | end 135 | end 136 | 137 | @doc """ 138 | Same as `new/1` but unwraps or raises on error. 139 | """ 140 | @spec new!(bag_options) :: Bag.t() 141 | def new!(opts \\ []), do: unwrap_or_raise(new(opts)) 142 | 143 | defp type(true), do: :duplicate_bag 144 | defp type(false), do: :bag 145 | 146 | @doc """ 147 | Returns information on the bag. 148 | 149 | Second parameter forces updated information from ets, default (false) uses in-struct cached information. 150 | Force should be used when requesting size and memory. 151 | 152 | ## Examples 153 | 154 | iex> {:ok, bag} = Bag.new(duplicate: false, keypos: 3, read_concurrency: true, compressed: false) 155 | iex> {:ok, info} = Bag.info(bag) 156 | iex> info[:read_concurrency] 157 | true 158 | iex> {:ok, _} = Bag.add(bag, {:a, :b, :c}) 159 | iex> {:ok, info} = Bag.info(bag) 160 | iex> info[:size] 161 | 0 162 | iex> {:ok, info} = Bag.info(bag, true) 163 | iex> info[:size] 164 | 1 165 | 166 | """ 167 | @spec info(Bag.t(), boolean()) :: {:ok, keyword()} | {:error, any()} 168 | def info(bag, force_update \\ false) 169 | def info(%Bag{table: table}, true), do: Base.info(table) 170 | def info(%Bag{info: info}, false), do: {:ok, info} 171 | 172 | @doc """ 173 | Same as `info/1` but unwraps or raises on error. 174 | """ 175 | @spec info!(Bag.t(), boolean()) :: keyword() 176 | def info!(%Bag{} = bag, force_update \\ false) when is_boolean(force_update), 177 | do: unwrap_or_raise(info(bag, force_update)) 178 | 179 | @doc """ 180 | Returns underlying `:ets` table reference. 181 | 182 | For use in functions that are not yet implemented. If you find yourself using this, please consider 183 | submitting a PR to add the necessary function to `ETS`. 184 | 185 | ## Examples 186 | 187 | iex> bag = Bag.new!(name: :my_ets_table) 188 | iex> {:ok, table} = Bag.get_table(bag) 189 | iex> info = :ets.info(table) 190 | iex> info[:name] 191 | :my_ets_table 192 | 193 | """ 194 | @spec get_table(Bag.t()) :: {:ok, ETS.table_reference()} 195 | def get_table(%Bag{table: table}), do: {:ok, table} 196 | 197 | @doc """ 198 | Same as `get_table/1` but unwraps or raises on error 199 | """ 200 | @spec get_table!(Bag.t()) :: ETS.table_reference() 201 | def get_table!(%Bag{} = bag), do: unwrap(get_table(bag)) 202 | 203 | @doc """ 204 | Adds tuple record or list of tuple records to table. 205 | 206 | If Bag has `duplicate: false`, will overwrite duplicate records (full tuple must match, not just key). 207 | 208 | Inserts multiple records in an [atomic and isolated](http://erlang.org/doc/man/ets.html#concurrency) manner. 209 | 210 | ## Examples 211 | 212 | iex> {:ok, bag} = Bag.new() 213 | iex> {:ok, _} = Bag.add(bag, [{:a, :b, :c}, {:d, :e, :f}]) 214 | iex> {:ok, _} = Bag.add(bag, {:a, :h, :i}) 215 | iex> {:ok, _} = Bag.add(bag, {:d, :x, :y}) 216 | iex> {:ok, _} = Bag.add(bag, {:d, :e, :f}) 217 | iex> Bag.to_list(bag) 218 | {:ok, [{:d, :e, :f}, {:d, :x, :y}, {:d, :e, :f}, {:a, :b, :c}, {:a, :h, :i}]} 219 | 220 | iex> {:ok, bag} = Bag.new(duplicate: false) 221 | iex> {:ok, _} = Bag.add(bag, [{:a, :b, :c}, {:d, :e, :f}]) 222 | iex> {:ok, _} = Bag.add(bag, {:a, :h, :i}) 223 | iex> {:ok, _} = Bag.add(bag, {:d, :x, :y}) 224 | iex> {:ok, _} = Bag.add(bag, {:d, :e, :f}) # won't insert due to duplicate: false 225 | iex> Bag.to_list(bag) 226 | {:ok, [{:d, :e, :f}, {:d, :x, :y}, {:a, :b, :c}, {:a, :h, :i}]} 227 | 228 | """ 229 | @spec add(Bag.t(), tuple() | list(tuple())) :: {:ok, Bag.t()} | {:error, any()} 230 | def add(%Bag{table: table} = bag, record) when is_tuple(record), 231 | do: Base.insert(table, record, bag) 232 | 233 | def add(%Bag{table: table} = bag, records) when is_list(records), 234 | do: Base.insert_multi(table, records, bag) 235 | 236 | @doc """ 237 | Same as `add/3` but unwraps or raises on error. 238 | """ 239 | @spec add!(Bag.t(), tuple() | list(tuple())) :: Bag.t() 240 | def add!(%Bag{} = bag, record_or_records) 241 | when is_tuple(record_or_records) or is_list(record_or_records), 242 | do: unwrap_or_raise(add(bag, record_or_records)) 243 | 244 | @doc """ 245 | Same as `add/2` but doesn't add any records if one of the given keys already exists. 246 | 247 | ## Examples 248 | 249 | iex> bag = Bag.new!() 250 | iex> {:ok, _} = Bag.add_new(bag, [{:a, :b, :c}, {:d, :e, :f}]) 251 | iex> {:ok, _} = Bag.add_new(bag, [{:a, :x, :y}, {:g, :h, :i}]) # skips due to duplicate :a key 252 | iex> {:ok, _} = Bag.add_new(bag, {:d, :z, :zz}) # skips due to duplicate :d key 253 | iex> Bag.to_list!(bag) 254 | [{:d, :e, :f}, {:a, :b, :c}] 255 | 256 | """ 257 | @spec add_new(Bag.t(), tuple() | list(tuple())) :: {:ok, Bag.t()} | {:error, any()} 258 | def add_new(%Bag{table: table} = bag, record) when is_tuple(record), 259 | do: Base.insert_new(table, record, bag) 260 | 261 | def add_new(%Bag{table: table} = bag, records) when is_list(records), 262 | do: Base.insert_multi_new(table, records, bag) 263 | 264 | @doc """ 265 | Same as `add_new/2` but unwraps or raises on error. 266 | """ 267 | @spec add_new!(Bag.t(), tuple() | list(tuple())) :: Bag.t() 268 | def add_new!(%Bag{} = bag, record_or_records) 269 | when is_tuple(record_or_records) or is_list(record_or_records), 270 | do: unwrap_or_raise(add_new(bag, record_or_records)) 271 | 272 | @doc """ 273 | Returns list of records with specified key. 274 | 275 | ## Examples 276 | 277 | iex> Bag.new!() 278 | iex> |> Bag.add!({:a, :b, :c}) 279 | iex> |> Bag.add!({:d, :e, :f}) 280 | iex> |> Bag.add!({:d, :e, :g}) 281 | iex> |> Bag.lookup(:d) 282 | {:ok, [{:d, :e, :f}, {:d, :e, :g}]} 283 | 284 | """ 285 | @spec lookup(Bag.t(), any()) :: {:ok, [tuple()]} | {:error, any()} 286 | def lookup(%Bag{table: table}, key), do: Base.lookup(table, key) 287 | 288 | @doc """ 289 | Same as `lookup/3` but unwraps or raises on error. 290 | """ 291 | @spec lookup!(Bag.t(), any()) :: [tuple()] 292 | def lookup!(%Bag{} = bag, key), do: unwrap_or_raise(lookup(bag, key)) 293 | 294 | @doc """ 295 | Returns list of elements in specified position of records with specified key. 296 | 297 | ## Examples 298 | 299 | iex> Bag.new!() 300 | iex> |> Bag.add!({:a, :b, :c}) 301 | iex> |> Bag.add!({:d, :e, :f}) 302 | iex> |> Bag.add!({:d, :h, :i}) 303 | iex> |> Bag.lookup_element(:d, 2) 304 | {:ok, [:e, :h]} 305 | 306 | """ 307 | @spec lookup_element(Bag.t(), any(), non_neg_integer()) :: {:ok, [any()]} | {:error, any()} 308 | def lookup_element(%Bag{table: table}, key, pos), do: Base.lookup_element(table, key, pos) 309 | 310 | @doc """ 311 | Same as `lookup_element/3` but unwraps or raises on error. 312 | """ 313 | @spec lookup_element!(Bag.t(), any(), non_neg_integer()) :: [any()] 314 | def lookup_element!(%Bag{} = bag, key, pos), do: unwrap_or_raise(lookup_element(bag, key, pos)) 315 | 316 | @doc """ 317 | Returns records in the Bag that match the specified pattern. 318 | 319 | For more information on the match pattern, see the [erlang documentation](http://erlang.org/doc/man/ets.html#match-2) 320 | 321 | ## Examples 322 | 323 | iex> Bag.new!() 324 | iex> |> Bag.add!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 325 | iex> |> Bag.match({:"$1", :b, :"$2", :_}) 326 | {:ok, [[:h, :i], [:a, :c]]} 327 | 328 | """ 329 | @spec match(Bag.t(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()} 330 | def match(%Bag{table: table}, pattern) when is_atom(pattern) or is_tuple(pattern), 331 | do: Base.match(table, pattern) 332 | 333 | @doc """ 334 | Same as `match/2` but unwraps or raises on error. 335 | """ 336 | @spec match!(Bag.t(), ETS.match_pattern()) :: [tuple()] 337 | def match!(%Bag{} = bag, pattern) when is_atom(pattern) or is_tuple(pattern), 338 | do: unwrap_or_raise(match(bag, pattern)) 339 | 340 | @doc """ 341 | Same as `match/2` but limits number of results to the specified limit. 342 | 343 | ## Examples 344 | 345 | iex> bag = Bag.new!() 346 | iex> Bag.add!(bag, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 347 | iex> {:ok, {results, _continuation}} = Bag.match(bag, {:"$1", :b, :"$2", :_}, 2) 348 | iex> results 349 | [[:e, :f], [:a, :c]] 350 | 351 | """ 352 | @spec match(Bag.t(), ETS.match_pattern(), non_neg_integer()) :: 353 | {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 354 | def match(%Bag{table: table}, pattern, limit) when is_atom(pattern) or is_tuple(pattern), 355 | do: Base.match(table, pattern, limit) 356 | 357 | @doc """ 358 | Same as `match/3` but unwraps or raises on error. 359 | """ 360 | @spec match!(Bag.t(), ETS.match_pattern(), non_neg_integer()) :: 361 | {[tuple()], any() | :end_of_table} 362 | def match!(%Bag{} = bag, pattern, limit) when is_atom(pattern) or is_tuple(pattern), 363 | do: unwrap_or_raise(match(bag, pattern, limit)) 364 | 365 | @doc """ 366 | Matches next bag of records from a match/3 or match/1 continuation. 367 | 368 | ## Examples 369 | 370 | iex> bag = Bag.new!() 371 | iex> Bag.add!(bag, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 372 | iex> {:ok, {results, continuation}} = Bag.match(bag, {:"$1", :b, :"$2", :_}, 2) 373 | iex> results 374 | [[:e, :f], [:a, :c]] 375 | iex> {:ok, {records2, continuation2}} = Bag.match(continuation) 376 | iex> records2 377 | [[:h, :i]] 378 | iex> continuation2 379 | :end_of_table 380 | 381 | """ 382 | @spec match(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 383 | def match(continuation), do: Base.match(continuation) 384 | 385 | @doc """ 386 | Same as `match/1` but unwraps or raises on error. 387 | """ 388 | @spec match!(any()) :: {[tuple()], any() | :end_of_table} 389 | def match!(continuation), do: unwrap_or_raise(match(continuation)) 390 | 391 | @doc """ 392 | Deletes all records that match the given pattern. 393 | 394 | Always returns `:ok`, regardless of whether anything was deleted or not. 395 | 396 | ## Examples 397 | 398 | iex> bag = Bag.new!() 399 | iex> Bag.add!(bag, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:a, :i, :j, :k}]) 400 | iex> Bag.match_delete(bag, {:_, :b, :_, :_}) 401 | {:ok, bag} 402 | iex> Bag.to_list!(bag) 403 | [{:a, :i, :j, :k}] 404 | 405 | """ 406 | @spec match_delete(Bag.t(), ETS.match_pattern()) :: {:ok, Bag.t()} | {:error, any()} 407 | def match_delete(%Bag{table: table} = bag, pattern) 408 | when is_atom(pattern) or is_tuple(pattern) do 409 | with :ok <- Base.match_delete(table, pattern) do 410 | {:ok, bag} 411 | end 412 | end 413 | 414 | @doc """ 415 | Same as `match_delete/2` but unwraps or raises on error. 416 | """ 417 | @spec match_delete!(Bag.t(), ETS.match_pattern()) :: Bag.t() 418 | def match_delete!(%Bag{} = bag, pattern) when is_atom(pattern) or is_tuple(pattern), 419 | do: unwrap_or_raise(match_delete(bag, pattern)) 420 | 421 | @doc """ 422 | Returns full records that match the specified pattern. 423 | 424 | For more information on the match pattern, see the [erlang documentation](http://erlang.org/doc/man/ets.html#match-2) 425 | 426 | ## Examples 427 | 428 | iex> Bag.new!() 429 | iex> |> Bag.add!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 430 | iex> |> Bag.match_object({:"$1", :b, :"$2", :_}) 431 | {:ok, [{:h, :b, :i, :j}, {:a, :b, :c, :d}]} 432 | 433 | """ 434 | @spec match_object(Bag.t(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()} 435 | def match_object(%Bag{table: table}, pattern) when is_atom(pattern) or is_tuple(pattern), 436 | do: Base.match_object(table, pattern) 437 | 438 | @doc """ 439 | Same as `match_object/2` but unwraps or raises on error. 440 | """ 441 | @spec match_object!(Bag.t(), ETS.match_pattern()) :: [tuple()] 442 | def match_object!(%Bag{} = bag, pattern) when is_atom(pattern) or is_tuple(pattern), 443 | do: unwrap_or_raise(match_object(bag, pattern)) 444 | 445 | @doc """ 446 | Same as `match/2` but limits number of results to the specified limit. 447 | 448 | ## Examples 449 | 450 | iex> bag = Bag.new!() 451 | iex> Bag.add!(bag, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 452 | iex> {:ok, {results, _continuation}} = Bag.match_object(bag, {:"$1", :b, :"$2", :_}, 2) 453 | iex> results 454 | [{:e, :b, :f, :g}, {:a, :b, :c, :d}] 455 | 456 | """ 457 | @spec match_object(Bag.t(), ETS.match_pattern(), non_neg_integer()) :: 458 | {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 459 | def match_object(%Bag{table: table}, pattern, limit) when is_atom(pattern) or is_tuple(pattern), 460 | do: Base.match_object(table, pattern, limit) 461 | 462 | @doc """ 463 | Same as `match_object/3` but unwraps or raises on error. 464 | """ 465 | @spec match_object!(Bag.t(), ETS.match_pattern(), non_neg_integer()) :: 466 | {[tuple()], any() | :end_of_table} 467 | def match_object!(%Bag{} = bag, pattern, limit) when is_atom(pattern) or is_tuple(pattern), 468 | do: unwrap_or_raise(match_object(bag, pattern, limit)) 469 | 470 | @doc """ 471 | Matches next records from a match/3 or match/1 continuation. 472 | 473 | ## Examples 474 | 475 | iex> bag = Bag.new!() 476 | iex> Bag.add!(bag, [{:a, :b, :c}, {:a, :b, :d}, {:a, :b, :e}, {:f, :b, :g}]) 477 | iex> {:ok, {results, continuation}} = Bag.match_object(bag, {:"$1", :b, :_}, 2) 478 | iex> results 479 | [{:a, :b, :d}, {:a, :b, :e}] 480 | iex> {:ok, {results2, continuation2}} = Bag.match_object(continuation) 481 | iex> results2 482 | [{:f, :b, :g}, {:a, :b, :c}] 483 | iex> {:ok, {[], :end_of_table}} = Bag.match_object(continuation2) 484 | 485 | """ 486 | @spec match_object(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 487 | def match_object(continuation), do: Base.match_object(continuation) 488 | 489 | @doc """ 490 | Same as `match_object/1` but unwraps or raises on error. 491 | """ 492 | @spec match_object!(any()) :: {[tuple()], any() | :end_of_table} 493 | def match_object!(continuation), do: unwrap_or_raise(match_object(continuation)) 494 | 495 | @doc """ 496 | Returns records in the specified Bag that match the specified match specification. 497 | 498 | For more information on the match specification, see the [erlang documentation](http://erlang.org/doc/man/ets.html#select-2) 499 | 500 | ## Examples 501 | 502 | iex> Bag.new!() 503 | iex> |> Bag.add!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 504 | iex> |> Bag.select([{{:"$1", :b, :"$2", :_},[],[:"$$"]}]) 505 | {:ok, [[:h, :i], [:a, :c]]} 506 | 507 | """ 508 | @spec select(Bag.t(), ETS.match_spec()) :: {:ok, [tuple()]} | {:error, any()} 509 | def select(%Bag{table: table}, spec) when is_list(spec), 510 | do: Base.select(table, spec) 511 | 512 | @doc """ 513 | Same as `select/2` but unwraps or raises on error. 514 | """ 515 | @spec select!(Bag.t(), ETS.match_spec()) :: [tuple()] 516 | def select!(%Bag{} = bag, spec) when is_list(spec), 517 | do: unwrap_or_raise(select(bag, spec)) 518 | 519 | @doc """ 520 | Deletes records in the specified Bag that match the specified match specification. 521 | 522 | For more information on the match specification, see the [erlang documentation](http://erlang.org/doc/man/ets.html#select_delete-2) 523 | 524 | ## Examples 525 | 526 | iex> bag = Bag.new!() 527 | iex> bag 528 | iex> |> Bag.add!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :c, :h}]) 529 | iex> |> Bag.select_delete([{{:"$1", :b, :"$2", :_},[{:"==", :"$2", :c}],[true]}]) 530 | {:ok, 2} 531 | iex> Bag.to_list!(bag) 532 | [{:e, :c, :f, :g}] 533 | 534 | """ 535 | @spec select_delete(Bag.t(), ETS.match_spec()) :: {:ok, non_neg_integer()} | {:error, any()} 536 | def select_delete(%Bag{table: table}, spec) when is_list(spec), 537 | do: Base.select_delete(table, spec) 538 | 539 | @doc """ 540 | Same as `select_delete/2` but unwraps or raises on error. 541 | """ 542 | @spec select_delete!(Bag.t(), ETS.match_spec()) :: non_neg_integer() 543 | def select_delete!(%Bag{} = bag, spec) when is_list(spec), 544 | do: unwrap_or_raise(select_delete(bag, spec)) 545 | 546 | @doc """ 547 | Determines if specified key exists in specified bag. 548 | 549 | ## Examples 550 | 551 | iex> bag = Bag.new!() 552 | iex> Bag.has_key(bag, :key) 553 | {:ok, false} 554 | iex> Bag.add(bag, {:key, :value}) 555 | iex> Bag.has_key(bag, :key) 556 | {:ok, true} 557 | 558 | """ 559 | @spec has_key(Bag.t(), any()) :: {:ok, boolean()} | {:error, any()} 560 | def has_key(%Bag{table: table}, key), do: Base.has_key(table, key) 561 | 562 | @doc """ 563 | Same as `has_key/2` but unwraps or raises on error. 564 | """ 565 | @spec has_key!(Bag.t(), any()) :: boolean() 566 | def has_key!(bag, key), do: unwrap_or_raise(has_key(bag, key)) 567 | 568 | @doc """ 569 | Returns contents of table as a list. 570 | 571 | ## Examples 572 | 573 | iex> Bag.new!() 574 | iex> |> Bag.add!({:a, :b, :c}) 575 | iex> |> Bag.add!({:d, :e, :f}) 576 | iex> |> Bag.add!({:d, :e, :f}) 577 | iex> |> Bag.to_list() 578 | {:ok, [{:d, :e, :f}, {:d, :e, :f}, {:a, :b, :c}]} 579 | 580 | """ 581 | @spec to_list(Bag.t()) :: {:ok, [tuple()]} | {:error, any()} 582 | def to_list(%Bag{table: table}), do: Base.to_list(table) 583 | 584 | @doc """ 585 | Same as `to_list/1` but unwraps or raises on error. 586 | """ 587 | @spec to_list!(Bag.t()) :: [tuple()] 588 | def to_list!(%Bag{} = bag), do: unwrap_or_raise(to_list(bag)) 589 | 590 | @doc """ 591 | Deletes specified Bag. 592 | 593 | ## Examples 594 | 595 | iex> {:ok, bag} = Bag.new() 596 | iex> {:ok, _} = Bag.info(bag, true) 597 | iex> {:ok, _} = Bag.delete(bag) 598 | iex> Bag.info(bag, true) 599 | {:error, :table_not_found} 600 | 601 | """ 602 | @spec delete(Bag.t()) :: {:ok, Bag.t()} | {:error, any()} 603 | def delete(%Bag{table: table} = bag), do: Base.delete(table, bag) 604 | 605 | @doc """ 606 | Same as `delete/1` but unwraps or raises on error. 607 | """ 608 | @spec delete!(Bag.t()) :: Bag.t() 609 | def delete!(%Bag{} = bag), do: unwrap_or_raise(delete(bag)) 610 | 611 | @doc """ 612 | Deletes record with specified key in specified Bag. 613 | 614 | ## Examples 615 | 616 | iex> bag = Bag.new!() 617 | iex> Bag.add(bag, {:a, :b, :c}) 618 | iex> Bag.delete(bag, :a) 619 | iex> Bag.lookup!(bag, :a) 620 | [] 621 | 622 | """ 623 | @spec delete(Bag.t(), any()) :: {:ok, Bag.t()} | {:error, any()} 624 | def delete(%Bag{table: table} = bag, key), do: Base.delete_records(table, key, bag) 625 | 626 | @doc """ 627 | Same as `delete/2` but unwraps or raises on error. 628 | """ 629 | @spec delete!(Bag.t(), any()) :: Bag.t() 630 | def delete!(%Bag{} = bag, key), do: unwrap_or_raise(delete(bag, key)) 631 | 632 | @doc """ 633 | Deletes all records in specified Bag. 634 | 635 | ## Examples 636 | 637 | iex> bag = Bag.new!() 638 | iex> bag 639 | iex> |> Bag.add!({:a, :b, :c}) 640 | iex> |> Bag.add!({:b, :b, :c}) 641 | iex> |> Bag.add!({:c, :b, :c}) 642 | iex> |> Bag.to_list!() 643 | [{:c, :b, :c}, {:b, :b, :c}, {:a, :b, :c}] 644 | iex> Bag.delete_all(bag) 645 | iex> Bag.to_list!(bag) 646 | [] 647 | 648 | """ 649 | @spec delete_all(Bag.t()) :: {:ok, Bag.t()} | {:error, any()} 650 | def delete_all(%Bag{table: table} = bag), do: Base.delete_all_records(table, bag) 651 | 652 | @doc """ 653 | Same as `delete_all/1` but unwraps or raises on error. 654 | """ 655 | @spec delete_all!(Bag.t()) :: Bag.t() 656 | def delete_all!(%Bag{} = bag), do: unwrap_or_raise(delete_all(bag)) 657 | 658 | @doc """ 659 | Wraps an existing :ets :bag or :duplicate_bag in a Bag struct. 660 | 661 | ## Examples 662 | 663 | iex> :ets.new(:my_ets_table, [:bag, :named_table]) 664 | iex> {:ok, bag} = Bag.wrap_existing(:my_ets_table) 665 | iex> Bag.info!(bag)[:name] 666 | :my_ets_table 667 | 668 | """ 669 | @spec wrap_existing(ETS.table_identifier()) :: {:ok, Bag.t()} | {:error, any()} 670 | def wrap_existing(table_identifier) do 671 | case Base.wrap_existing(table_identifier, [:bag, :duplicate_bag]) do 672 | {:ok, {table, info}} -> 673 | {:ok, %Bag{table: table, info: info, duplicate: info[:type] == :duplicate_bag}} 674 | 675 | {:error, reason} -> 676 | {:error, reason} 677 | end 678 | end 679 | 680 | @doc """ 681 | Same as `wrap_existing/1` but unwraps or raises on error. 682 | """ 683 | @spec wrap_existing!(ETS.table_identifier()) :: Bag.t() 684 | def wrap_existing!(table_identifier), do: unwrap_or_raise(wrap_existing(table_identifier)) 685 | 686 | @doc """ 687 | Transfers ownership of a Bag to another process. 688 | 689 | ## Examples 690 | 691 | iex> bag = Bag.new!() 692 | iex> receiver_pid = spawn(fn -> Bag.accept() end) 693 | iex> Bag.give_away(bag, receiver_pid) 694 | {:ok, bag} 695 | 696 | iex> bag = Bag.new!() 697 | iex> dead_pid = ETS.TestUtils.dead_pid() 698 | iex> Bag.give_away(bag, dead_pid) 699 | {:error, :recipient_not_alive} 700 | 701 | """ 702 | @spec give_away(Bag.t(), pid(), any()) :: {:ok, Bag.t()} | {:error, any()} 703 | def give_away(%Bag{table: table} = bag, pid, gift \\ []), 704 | do: Base.give_away(table, pid, gift, bag) 705 | 706 | @doc """ 707 | Same as `give_away/3` but unwraps or raises on error. 708 | """ 709 | @spec give_away!(Bag.t(), pid(), any()) :: Bag.t() 710 | def give_away!(%Bag{} = bag, pid, gift \\ []), 711 | do: unwrap_or_raise(give_away(bag, pid, gift)) 712 | 713 | @doc """ 714 | Waits to accept ownership of a table after it is given away. Successful receipt will 715 | return `{:ok, %{bag: bag, from: from, gift: gift}}` where `from` is the pid of the previous 716 | owner, and `gift` is any additional metadata sent with the table. 717 | 718 | A timeout may be given in milliseconds, which will return `{:error, :timeout}` if reached. 719 | 720 | See `give_away/3` for more information. 721 | """ 722 | @spec accept() :: {:ok, Bag.t(), pid(), any()} | {:error, any()} 723 | def accept(timeout \\ :infinity) do 724 | with {:ok, table, from, gift} <- Base.accept(timeout), 725 | {:ok, bag} <- Bag.wrap_existing(table) do 726 | {:ok, %{bag: bag, from: from, gift: gift}} 727 | end 728 | end 729 | 730 | @doc """ 731 | For processes which may receive ownership of a Bag unexpectedly - either via `give_away/3` or 732 | by being named the Bag's heir (see `new/1`) - the module should include at least one `accept` 733 | clause. For example, if we want a server to inherit Bags after their previous owner dies: 734 | 735 | ``` 736 | defmodule Receiver do 737 | use GenServer 738 | alias ETS.Bag 739 | require ETS.Bag 740 | 741 | ... 742 | 743 | Bag.accept :owner_crashed, bag, _from, state do 744 | new_state = Map.update!(state, :crashed_bags, &[bag | &1]) 745 | {:noreply, new_state} 746 | end 747 | ``` 748 | 749 | The first argument is a unique identifier which should match either the "heir_data" 750 | in `new/1`, or the "gift" in `give_away/3`. 751 | The other arguments declare the variables which may be used in the `do` block: 752 | the received Bag, the pid of the previous owner, and the current state of the process. 753 | 754 | The return value should be in the form {:noreply, new_state}, or one of the similar 755 | returns expected by `handle_info`/`handle_cast`. 756 | """ 757 | defmacro accept(id, table, from, state, do: contents) do 758 | quote do 759 | require Base 760 | 761 | Base.accept unquote(id), unquote(table), unquote(from), unquote(state) do 762 | var!(unquote(table)) = Bag.wrap_existing!(unquote(table)) 763 | unquote(contents) 764 | end 765 | end 766 | end 767 | end 768 | -------------------------------------------------------------------------------- /lib/ets/base.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.Base do 2 | @moduledoc """ 3 | Base implementation for table modules (e.g. `ETS.Set` and `ETS.Bag`). Should not be used directly. 4 | 5 | """ 6 | use ETS.Utils 7 | 8 | @protection_types [:public, :protected, :private] 9 | @type option :: 10 | {:name, atom()} 11 | | {:protection, :private | :protected | :public} 12 | | {:heir, :none | {pid(), any()}} 13 | | {:keypos, non_neg_integer()} 14 | | {:write_concurrency, boolean() | :auto} 15 | | {:read_concurrency, boolean()} 16 | | {:compressed, boolean()} 17 | @type options :: [option] 18 | @type table_types :: :bag | :duplicate_bag | :ordered_set | :set 19 | @table_types [:bag, :duplicate_bag, :ordered_set, :set] 20 | 21 | @doc false 22 | @spec new_table(table_types(), keyword()) :: 23 | {:ok, {ETS.table_reference(), keyword()}} | {:error, any()} 24 | def new_table(type, opts) when type in @table_types and is_list(opts) do 25 | {opts, name} = take_opt(opts, :name, nil) 26 | 27 | if is_atom(name) do 28 | starting_opts = 29 | if is_nil(name) do 30 | [type] 31 | else 32 | [:named_table, type] 33 | end 34 | 35 | case parse_opts(starting_opts, opts) do 36 | {:ok, parsed_opts} -> 37 | catch_table_already_exists name do 38 | info = 39 | name 40 | |> :ets.new(parsed_opts) 41 | |> :ets.info() 42 | 43 | ref = info[:id] 44 | {:ok, {ref, info}} 45 | end 46 | 47 | {:error, reason} -> 48 | {:error, reason} 49 | end 50 | else 51 | {:error, {:invalid_option, {:name, name}}} 52 | end 53 | end 54 | 55 | @spec parse_opts(list(), options) :: {:ok, list()} | {:error, {:invalid_option, any()}} 56 | defp parse_opts(acc, [{:protection, protection} | tl]) when protection in @protection_types, 57 | do: parse_opts([protection | acc], tl) 58 | 59 | defp parse_opts(acc, [{:heir, {pid, heir_data}} | tl]) when is_pid(pid), 60 | do: parse_opts([{:heir, pid, heir_data} | acc], tl) 61 | 62 | defp parse_opts(acc, [{:heir, :none} | tl]), do: parse_opts([{:heir, :none} | acc], tl) 63 | 64 | defp parse_opts(acc, [{:keypos, keypos} | tl]) when is_integer(keypos) and keypos >= 0, 65 | do: parse_opts([{:keypos, keypos} | acc], tl) 66 | 67 | defp parse_opts(acc, [{:write_concurrency, wc} | tl]) when is_boolean(wc) or wc == :auto, 68 | do: parse_opts([{:write_concurrency, wc} | acc], tl) 69 | 70 | defp parse_opts(acc, [{:read_concurrency, rc} | tl]) when is_boolean(rc), 71 | do: parse_opts([{:read_concurrency, rc} | acc], tl) 72 | 73 | defp parse_opts(acc, [{:compressed, true} | tl]), do: parse_opts([:compressed | acc], tl) 74 | defp parse_opts(acc, [{:compressed, false} | tl]), do: parse_opts(acc, tl) 75 | 76 | defp parse_opts(acc, []), do: {:ok, acc} 77 | 78 | defp parse_opts(_, [bad_val | _]), 79 | do: {:error, {:invalid_option, bad_val}} 80 | 81 | @doc false 82 | @spec info(ETS.table_identifier()) :: {:ok, keyword()} | {:error, :table_not_found} 83 | def info(table) do 84 | catch_error do 85 | case :ets.info(table) do 86 | :undefined -> {:error, :table_not_found} 87 | x -> {:ok, x} 88 | end 89 | end 90 | end 91 | 92 | @doc false 93 | @spec insert(ETS.table_identifier(), tuple(), any()) :: {:ok, any()} | {:error, any()} 94 | def insert(table, record, return) do 95 | catch_error do 96 | catch_write_protected table do 97 | catch_record_too_small table, record do 98 | catch_table_not_found table do 99 | :ets.insert(table, record) 100 | {:ok, return} 101 | end 102 | end 103 | end 104 | end 105 | end 106 | 107 | @doc false 108 | @spec insert_new(ETS.table_identifier(), tuple(), any()) :: {:ok, any()} | {:error, any()} 109 | def insert_new(table, record, return) do 110 | catch_error do 111 | catch_write_protected table do 112 | catch_record_too_small table, record do 113 | catch_table_not_found table do 114 | :ets.insert_new(table, record) 115 | {:ok, return} 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | @doc false 123 | @spec insert_multi(ETS.table_identifier(), list(tuple()), any()) :: 124 | {:ok, any()} | {:error, any()} 125 | def insert_multi(table, records, return) do 126 | catch_error do 127 | catch_write_protected table do 128 | catch_records_too_small table, records do 129 | catch_bad_records records do 130 | catch_table_not_found table do 131 | :ets.insert(table, records) 132 | {:ok, return} 133 | end 134 | end 135 | end 136 | end 137 | end 138 | end 139 | 140 | @doc false 141 | @spec insert_multi_new(ETS.table_identifier(), list(tuple), any()) :: 142 | {:ok, any()} | {:error, any()} 143 | def insert_multi_new(table, records, return) do 144 | catch_error do 145 | catch_write_protected table do 146 | catch_records_too_small table, records do 147 | catch_bad_records records do 148 | catch_table_not_found table do 149 | :ets.insert_new(table, records) 150 | {:ok, return} 151 | end 152 | end 153 | end 154 | end 155 | end 156 | end 157 | 158 | @doc false 159 | @spec to_list(ETS.table_identifier()) :: {:ok, [tuple()]} | {:error, any()} 160 | def to_list(table) do 161 | catch_error do 162 | catch_table_not_found table do 163 | {:ok, :ets.tab2list(table)} 164 | end 165 | end 166 | end 167 | 168 | @doc false 169 | @spec lookup(ETS.table_identifier(), any()) :: {:ok, [tuple()]} | {:error, any()} 170 | def lookup(table, key) do 171 | catch_error do 172 | catch_read_protected table do 173 | catch_table_not_found table do 174 | vals = :ets.lookup(table, key) 175 | {:ok, vals} 176 | end 177 | end 178 | end 179 | end 180 | 181 | @doc false 182 | @spec lookup_element(ETS.table_identifier(), any(), non_neg_integer()) :: 183 | {:ok, any()} | {:error, any()} 184 | def lookup_element(table, key, pos) do 185 | catch_error do 186 | catch_position_out_of_bounds table, key, pos do 187 | catch_key_not_found table, key do 188 | catch_read_protected table do 189 | catch_table_not_found table do 190 | vals = :ets.lookup_element(table, key, pos) 191 | {:ok, vals} 192 | end 193 | end 194 | end 195 | end 196 | end 197 | end 198 | 199 | @doc false 200 | @spec match(ETS.table_identifier(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()} 201 | def match(table, pattern) do 202 | catch_error do 203 | catch_read_protected table do 204 | catch_table_not_found table do 205 | matches = :ets.match(table, pattern) 206 | {:ok, matches} 207 | end 208 | end 209 | end 210 | end 211 | 212 | @doc false 213 | @spec match(ETS.table_identifier(), ETS.match_pattern(), non_neg_integer()) :: 214 | {:ok, {[tuple()], any()}} | {:error, any()} 215 | def match(table, pattern, limit) do 216 | catch_error do 217 | catch_read_protected table do 218 | catch_table_not_found table do 219 | case :ets.match(table, pattern, limit) do 220 | {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}} 221 | {records, continuation} -> {:ok, {records, continuation}} 222 | :"$end_of_table" -> {:ok, {[], :end_of_table}} 223 | end 224 | end 225 | end 226 | end 227 | end 228 | 229 | @doc false 230 | @spec match(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 231 | def match(continuation) do 232 | catch_error do 233 | try do 234 | case :ets.match(continuation) do 235 | {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}} 236 | {records, continuation} -> {:ok, {records, continuation}} 237 | :"$end_of_table" -> {:ok, {[], :end_of_table}} 238 | end 239 | rescue 240 | ArgumentError -> 241 | {:error, :invalid_continuation} 242 | end 243 | end 244 | end 245 | 246 | @doc false 247 | @spec match_delete(ETS.table_identifier(), ETS.match_pattern()) :: :ok | {:error, any()} 248 | def match_delete(table, pattern) do 249 | catch_error do 250 | catch_read_protected table do 251 | catch_table_not_found table do 252 | :ets.match_delete(table, pattern) 253 | :ok 254 | end 255 | end 256 | end 257 | end 258 | 259 | @doc false 260 | @spec match_object(ETS.table_identifier(), ETS.match_pattern()) :: 261 | {:ok, [tuple()]} | {:error, any()} 262 | def match_object(table, pattern) do 263 | catch_error do 264 | catch_read_protected table do 265 | catch_table_not_found table do 266 | matches = :ets.match_object(table, pattern) 267 | {:ok, matches} 268 | end 269 | end 270 | end 271 | end 272 | 273 | @doc false 274 | @spec match_object(ETS.table_identifier(), ETS.match_pattern(), non_neg_integer()) :: 275 | {:ok, {[tuple()], any()}} | {:error, any()} 276 | def match_object(table, pattern, limit) do 277 | catch_error do 278 | catch_read_protected table do 279 | catch_table_not_found table do 280 | case :ets.match_object(table, pattern, limit) do 281 | {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}} 282 | {records, continuation} -> {:ok, {records, continuation}} 283 | :"$end_of_table" -> {:ok, {[], :end_of_table}} 284 | end 285 | end 286 | end 287 | end 288 | end 289 | 290 | @doc false 291 | @spec match_object(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 292 | def match_object(continuation) do 293 | catch_error do 294 | try do 295 | case :ets.match_object(continuation) do 296 | {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}} 297 | {records, continuation} -> {:ok, {records, continuation}} 298 | :"$end_of_table" -> {:ok, {[], :end_of_table}} 299 | end 300 | rescue 301 | ArgumentError -> 302 | {:error, :invalid_continuation} 303 | end 304 | end 305 | end 306 | 307 | @spec select(ETS.continuation()) :: 308 | {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()} 309 | def select(continuation) do 310 | catch_error do 311 | catch_invalid_continuation continuation do 312 | matches = :ets.select(continuation) 313 | {:ok, matches} 314 | end 315 | end 316 | end 317 | 318 | @doc false 319 | @spec select(ETS.table_identifier(), ETS.match_spec()) :: {:ok, [tuple()]} | {:error, any()} 320 | def select(table, spec) when is_list(spec) do 321 | catch_error do 322 | catch_read_protected table do 323 | catch_invalid_select_spec spec do 324 | catch_table_not_found table do 325 | matches = :ets.select(table, spec) 326 | {:ok, matches} 327 | end 328 | end 329 | end 330 | end 331 | end 332 | 333 | @doc false 334 | @spec select(ETS.table_identifier(), ETS.match_spec(), limit :: integer) :: 335 | {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()} 336 | def select(table, spec, limit) when is_list(spec) do 337 | catch_error do 338 | catch_read_protected table do 339 | catch_invalid_select_spec spec do 340 | catch_table_not_found table do 341 | matches = :ets.select(table, spec, limit) 342 | {:ok, matches} 343 | end 344 | end 345 | end 346 | end 347 | end 348 | 349 | @doc false 350 | @spec select_delete(ETS.table_identifier(), ETS.match_spec()) :: 351 | {:ok, non_neg_integer()} | {:error, any()} 352 | def select_delete(table, spec) when is_list(spec) do 353 | catch_error do 354 | catch_read_protected table do 355 | catch_invalid_select_spec spec do 356 | catch_table_not_found table do 357 | count = :ets.select_delete(table, spec) 358 | {:ok, count} 359 | end 360 | end 361 | end 362 | end 363 | end 364 | 365 | @doc false 366 | @spec has_key(ETS.table_identifier(), any()) :: {:ok, boolean()} | {:error, any()} 367 | def has_key(table, key) do 368 | catch_error do 369 | catch_read_protected table do 370 | catch_table_not_found table do 371 | {:ok, :ets.member(table, key)} 372 | end 373 | end 374 | end 375 | end 376 | 377 | @doc false 378 | @spec first(ETS.table_identifier()) :: {:ok, any()} | {:error, any()} 379 | def first(table) do 380 | catch_error do 381 | catch_read_protected table do 382 | catch_table_not_found table do 383 | case :ets.first(table) do 384 | :"$end_of_table" -> {:error, :empty_table} 385 | x -> {:ok, x} 386 | end 387 | end 388 | end 389 | end 390 | end 391 | 392 | @doc false 393 | @spec last(ETS.table_identifier()) :: {:ok, any()} | {:error, any()} 394 | def last(table) do 395 | catch_error do 396 | catch_read_protected table do 397 | catch_table_not_found table do 398 | case :ets.last(table) do 399 | :"$end_of_table" -> {:error, :empty_table} 400 | x -> {:ok, x} 401 | end 402 | end 403 | end 404 | end 405 | end 406 | 407 | @doc false 408 | @spec next(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()} 409 | def next(table, key) do 410 | catch_error do 411 | catch_read_protected table do 412 | catch_table_not_found table do 413 | case :ets.next(table, key) do 414 | :"$end_of_table" -> {:error, :end_of_table} 415 | x -> {:ok, x} 416 | end 417 | end 418 | end 419 | end 420 | end 421 | 422 | @doc false 423 | @spec previous(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()} 424 | def previous(table, key) do 425 | catch_error do 426 | catch_read_protected table do 427 | catch_table_not_found table do 428 | case :ets.prev(table, key) do 429 | :"$end_of_table" -> {:error, :start_of_table} 430 | x -> {:ok, x} 431 | end 432 | end 433 | end 434 | end 435 | end 436 | 437 | @doc false 438 | @spec delete(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()} 439 | def delete(table, return) do 440 | catch_error do 441 | catch_write_protected table do 442 | catch_table_not_found table do 443 | :ets.delete(table) 444 | {:ok, return} 445 | end 446 | end 447 | end 448 | end 449 | 450 | @doc false 451 | @spec delete_records(ETS.table_identifier(), any(), any()) :: {:ok, any()} | {:error, any()} 452 | def delete_records(table, key, return) do 453 | catch_error do 454 | catch_write_protected table do 455 | catch_table_not_found table do 456 | :ets.delete(table, key) 457 | {:ok, return} 458 | end 459 | end 460 | end 461 | end 462 | 463 | @doc false 464 | @spec delete_all_records(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()} 465 | def delete_all_records(table, return) do 466 | catch_error do 467 | catch_write_protected table do 468 | catch_table_not_found table do 469 | :ets.delete_all_objects(table) 470 | {:ok, return} 471 | end 472 | end 473 | end 474 | end 475 | 476 | @doc false 477 | @spec wrap_existing(ETS.table_identifier(), [table_types]) :: 478 | {:ok, {ETS.table_reference(), keyword()}} | {:error, any()} 479 | def wrap_existing(table, valid_types) do 480 | catch_error do 481 | catch_table_not_found table do 482 | case :ets.info(table) do 483 | :undefined -> 484 | {:error, :table_not_found} 485 | 486 | info -> 487 | if info[:type] in valid_types do 488 | {:ok, {info[:id], info}} 489 | else 490 | {:error, :invalid_type} 491 | end 492 | end 493 | end 494 | end 495 | end 496 | 497 | @doc false 498 | @spec update_element(ETS.table_identifier(), any(), tuple() | [tuple()]) :: 499 | boolean() | {:error, any()} 500 | def update_element(table, key, element_spec) do 501 | catch_error do 502 | catch_key_update table, element_spec do 503 | catch_positions_out_of_bounds table, key, element_spec do 504 | catch_write_protected table do 505 | catch_table_not_found table do 506 | :ets.update_element(table, key, element_spec) 507 | end 508 | end 509 | end 510 | end 511 | end 512 | end 513 | 514 | @spec give_away(ETS.table_identifier(), pid(), any(), any()) :: {:ok, any()} | {:error, any()} 515 | def give_away(table, pid, gift, return) do 516 | catch_error do 517 | catch_sender_not_table_owner table do 518 | catch_recipient_not_alive pid do 519 | catch_recipient_already_owns_table table, pid do 520 | catch_table_not_found table do 521 | :ets.give_away(table, pid, gift) 522 | {:ok, return} 523 | end 524 | end 525 | end 526 | end 527 | end 528 | end 529 | 530 | @doc false 531 | @spec accept(integer() | :infinity) :: 532 | {:ok, ETS.table_identifier(), pid(), any()} | {:error, :timeout} 533 | def accept(timeout) do 534 | receive do 535 | {:"ETS-TRANSFER", table, from, gift} -> 536 | {:ok, table, from, gift} 537 | after 538 | timeout -> 539 | {:error, :timeout} 540 | end 541 | end 542 | 543 | defmacro accept(id, table, from, state, do: contents) do 544 | quote do 545 | def handle_info( 546 | {:"ETS-TRANSFER", unquote(table), unquote(from), unquote(id)}, 547 | unquote(state) 548 | ) do 549 | var!(unquote(table)) = unquote(table) 550 | unquote(contents) 551 | end 552 | end 553 | end 554 | end 555 | -------------------------------------------------------------------------------- /lib/ets/key_value_set.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.KeyValueSet do 2 | @moduledoc """ 3 | The Key Value Set is an extension of `ETS.Set` which abstracts the concept of tuple records 4 | away, replacing it with the standard concept of key/value. Behind the scenes, the set stores 5 | its records as {key, value}. 6 | 7 | ## Examples 8 | 9 | iex> {:ok, kvset} = KeyValueSet.new() 10 | iex> KeyValueSet.put(kvset, :my_key, :my_val) 11 | iex> KeyValueSet.get(kvset, :my_key) 12 | {:ok, :my_val} 13 | 14 | `KeyValueSet` implements [`Access`] _behaviour_. 15 | 16 | ## Examples 17 | 18 | iex> set = 19 | ...> KeyValueSet.new!() 20 | ...> |> KeyValueSet.put!(:k1, :v1) 21 | ...> |> KeyValueSet.put!(:k2, :v2) 22 | ...> |> KeyValueSet.put!(:k3, :v3) 23 | iex> get_in(set, [:k1]) 24 | :v1 25 | iex> get_in(set, [:z]) 26 | nil 27 | iex> with {:v2, set} <- 28 | ...> pop_in(set, [:k2]), do: KeyValueSet.to_list!(set) 29 | [k3: :v3, k1: :v1] 30 | iex> with {nil, set} <- pop_in(set, [:z]), do: KeyValueSet.to_list!(set) 31 | [k3: :v3, k1: :v1] 32 | iex> with {:v1, set} <- 33 | ...> get_and_update_in(set, [:k1], &{&1, :v42}), 34 | ...> do: KeyValueSet.to_list!(set) 35 | [k3: :v3, k1: :v42] 36 | iex> with {:v42, set} <- 37 | ...> get_and_update_in(set, [:k1], fn _ -> :pop end), 38 | ...> do: KeyValueSet.to_list!(set) 39 | [k3: :v3] 40 | 41 | """ 42 | use ETS.Utils 43 | use ETS.KeyValueSet.Macros 44 | 45 | @behaviour Access 46 | 47 | alias ETS.Base 48 | alias ETS.KeyValueSet 49 | alias ETS.Set 50 | 51 | @type t :: %__MODULE__{ 52 | set: Set.t() 53 | } 54 | 55 | @type set_options :: [ETS.Base.option() | {:ordered, boolean()}] 56 | 57 | defstruct set: nil 58 | 59 | @doc """ 60 | Creates new Key Value Set module with the specified options. 61 | 62 | Possible Options can be found in `ETS.Set` with the difference that specifying a `keypos` 63 | will result in an error. 64 | 65 | ## Examples 66 | 67 | iex> {:ok, kvset} = KeyValueSet.new(ordered: true,read_concurrency: true, compressed: false) 68 | iex> KeyValueSet.info!(kvset)[:read_concurrency] 69 | true 70 | 71 | # Named :ets tables via the name keyword 72 | iex> {:ok, kvset} = KeyValueSet.new(name: :my_ets_table) 73 | iex> KeyValueSet.info!(kvset)[:name] 74 | :my_ets_table 75 | 76 | """ 77 | @spec new(set_options) :: {:error, any()} | {:ok, KeyValueSet.t()} 78 | def new(opts \\ []) when is_list(opts) do 79 | with( 80 | {:keypos, false} <- {:keypos, Keyword.has_key?(opts, :keypos)}, 81 | {:ok, set} <- Set.new(opts) 82 | ) do 83 | {:ok, %KeyValueSet{set: set}} 84 | else 85 | {:keypos, true} -> {:error, {:invalid_option, {:keypos, Keyword.get(opts, :keypos)}}} 86 | {:error, reason} -> {:error, reason} 87 | end 88 | end 89 | 90 | @doc """ 91 | Same as `new/1` but unwraps or raises on error. 92 | """ 93 | @spec new!(set_options) :: KeyValueSet.t() 94 | def new!(opts \\ []), do: unwrap_or_raise(new(opts)) 95 | 96 | @doc """ 97 | Wraps an existing :ets :set or :ordered_set in a KeyValueSet struct. 98 | 99 | ## Examples 100 | 101 | iex> :ets.new(:my_ets_table, [:set, :named_table]) 102 | iex> {:ok, set} = KeyValueSet.wrap_existing(:my_ets_table) 103 | iex> KeyValueSet.info!(set)[:name] 104 | :my_ets_table 105 | 106 | """ 107 | @spec wrap_existing(ETS.table_identifier()) :: {:ok, KeyValueSet.t()} | {:error, any()} 108 | def wrap_existing(table_identifier) do 109 | with( 110 | {:ok, set} <- Set.wrap_existing(table_identifier), 111 | {:ok, info} <- Set.info(set), 112 | {:keypos, true} <- {:keypos, info[:keypos] == 1} 113 | ) do 114 | {:ok, %KeyValueSet{set: set}} 115 | else 116 | {:keypos, false} -> {:error, :invalid_keypos} 117 | {:error, reason} -> {:error, reason} 118 | end 119 | end 120 | 121 | @doc """ 122 | Same as `wrap_existing/1` but unwraps or raises on error. 123 | """ 124 | @spec wrap_existing!(ETS.table_identifier()) :: KeyValueSet.t() 125 | def wrap_existing!(table_identifier), do: unwrap_or_raise(wrap_existing(table_identifier)) 126 | 127 | @doc """ 128 | Puts given value into table for given key. 129 | 130 | ## Examples 131 | 132 | iex> kvset = KeyValueSet.new!(ordered: true) 133 | iex> {:ok, kvset} = KeyValueSet.put(kvset, :a, :b) 134 | iex> KeyValueSet.get!(kvset, :a) 135 | :b 136 | 137 | """ 138 | def put(%KeyValueSet{set: set} = key_value_set, key, value) do 139 | case Set.put(set, {key, value}) do 140 | {:ok, _} -> {:ok, key_value_set} 141 | {:error, reason} -> {:error, reason} 142 | end 143 | end 144 | 145 | @doc """ 146 | Same as `put/3` but unwraps or raises on error. 147 | """ 148 | @spec put!(KeyValueSet.t(), any(), any()) :: KeyValueSet.t() 149 | def put!(%KeyValueSet{} = key_value_set, key, value), 150 | do: unwrap_or_raise(put(key_value_set, key, value)) 151 | 152 | @doc """ 153 | Same as `put/3` but doesn't put record if the key already exists. 154 | 155 | ## Examples 156 | 157 | iex> set = KeyValueSet.new!(ordered: true) 158 | iex> {:ok, _} = KeyValueSet.put_new(set, :a, :b) 159 | iex> {:ok, _} = KeyValueSet.put_new(set, :a, :c) # skips due toduplicate :a key 160 | iex> KeyValueSet.to_list!(set) 161 | [{:a, :b}] 162 | 163 | """ 164 | @spec put_new(KeyValueSet.t(), any(), any()) :: {:ok, KeyValueSet.t()} | {:error, any()} 165 | def put_new(%KeyValueSet{set: set} = key_value_set, key, value) do 166 | case Set.put_new(set, {key, value}) do 167 | {:ok, _} -> {:ok, key_value_set} 168 | {:error, reason} -> {:error, reason} 169 | end 170 | end 171 | 172 | @doc """ 173 | Same as `put_new/3` but unwraps or raises on error. 174 | """ 175 | @spec put_new!(KeyValueSet.t(), any(), any()) :: KeyValueSet.t() 176 | def put_new!(%KeyValueSet{} = key_value_set, key, value), 177 | do: unwrap_or_raise(put_new(key_value_set, key, value)) 178 | 179 | @doc """ 180 | Returns value for specified key or the provided default (nil if not specified) if no record found. 181 | 182 | ## Examples 183 | 184 | iex> KeyValueSet.new!() 185 | iex> |> KeyValueSet.put!(:a, :b) 186 | iex> |> KeyValueSet.put!(:c, :d) 187 | iex> |> KeyValueSet.put!(:e, :f) 188 | iex> |> KeyValueSet.get(:c) 189 | {:ok, :d} 190 | 191 | """ 192 | @spec get(KeyValueSet.t(), any(), any()) :: {:ok, any()} | {:error, any()} 193 | def get(%KeyValueSet{set: set}, key, default \\ nil) do 194 | case Set.get(set, key, default) do 195 | {:ok, {_, value}} -> {:ok, value} 196 | {:ok, ^default} -> {:ok, default} 197 | {:error, reason} -> {:error, reason} 198 | end 199 | end 200 | 201 | @doc """ 202 | Same as `get/3` but unwraps or raises on error 203 | """ 204 | @spec get!(KeyValueSet.t(), any(), any()) :: any() 205 | def get!(%KeyValueSet{} = key_value_set, key, default \\ nil), 206 | do: unwrap_or_raise(get(key_value_set, key, default)) 207 | 208 | @doc """ 209 | Deletes record with specified key in specified Set. 210 | 211 | ## Examples 212 | 213 | iex> set = KeyValueSet.new!() 214 | iex> KeyValueSet.put(set, :a, :b) 215 | iex> KeyValueSet.delete(set, :a) 216 | iex> KeyValueSet.get!(set, :a) 217 | nil 218 | 219 | """ 220 | @spec delete(KeyValueSet.t(), any()) :: {:ok, KeyValueSet.t()} | {:error, any()} 221 | def delete(%KeyValueSet{set: set}, key) do 222 | with {:ok, %Set{table: table}} <- Set.delete(set, key), 223 | do: KeyValueSet.wrap_existing(table) 224 | end 225 | 226 | @doc """ 227 | Same as `delete/2` but unwraps or raises on error. 228 | """ 229 | @spec delete!(KeyValueSet.t(), any()) :: KeyValueSet.t() 230 | def delete!(%KeyValueSet{} = set, key), 231 | do: unwrap_or_raise(delete(set, key)) 232 | 233 | @doc """ 234 | Deletes all records in specified Set. 235 | 236 | ## Examples 237 | 238 | iex> set = KeyValueSet.new!() 239 | iex> set 240 | iex> |> KeyValueSet.put!(:a, :d) 241 | iex> |> KeyValueSet.put!(:b, :d) 242 | iex> |> KeyValueSet.put!(:c, :d) 243 | iex> |> KeyValueSet.to_list!() 244 | [c: :d, b: :d, a: :d] 245 | iex> KeyValueSet.delete_all(set) 246 | iex> KeyValueSet.to_list!(set) 247 | [] 248 | 249 | """ 250 | @spec delete_all(KeyValueSet.t()) :: {:ok, KeyValueSet.t()} | {:error, any()} 251 | def delete_all(%KeyValueSet{set: set}) do 252 | with {:ok, %Set{table: table}} <- Set.delete_all(set), 253 | do: KeyValueSet.wrap_existing(table) 254 | end 255 | 256 | @doc """ 257 | Same as `delete_all/1` but unwraps or raises on error. 258 | """ 259 | @spec delete_all!(KeyValueSet.t()) :: KeyValueSet.t() 260 | def delete_all!(%KeyValueSet{} = set), 261 | do: unwrap_or_raise(delete_all(set)) 262 | 263 | def info(key_value_set, force_update \\ false) 264 | def info!(key_value_set, force_update \\ false) 265 | 266 | @doc """ 267 | Transfers ownership of a KeyValueSet to another process. 268 | 269 | ## Examples 270 | 271 | iex> kv_set = KeyValueSet.new!() 272 | iex> receiver_pid = spawn(fn -> KeyValueSet.accept() end) 273 | iex> KeyValueSet.give_away(kv_set, receiver_pid) 274 | {:ok, kv_set} 275 | 276 | iex> kv_set = KeyValueSet.new!() 277 | iex> dead_pid = ETS.TestUtils.dead_pid() 278 | iex> KeyValueSet.give_away(kv_set, dead_pid) 279 | {:error, :recipient_not_alive} 280 | 281 | """ 282 | @spec give_away(KeyValueSet.t(), pid(), any()) :: {:ok, KeyValueSet.t()} | {:error, any()} 283 | def give_away(%KeyValueSet{set: set}, pid, gift \\ []) do 284 | with {:ok, set} <- Set.give_away(set, pid, gift), 285 | do: {:ok, %KeyValueSet{set: set}} 286 | end 287 | 288 | @doc """ 289 | Same as `give_away/3` but unwraps or raises on error. 290 | """ 291 | @spec give_away!(KeyValueSet.t(), pid(), any()) :: KeyValueSet.t() 292 | def give_away!(%KeyValueSet{} = kv_set, pid, gift \\ []), 293 | do: unwrap_or_raise(give_away(kv_set, pid, gift)) 294 | 295 | @doc """ 296 | Waits to accept ownership of a table after it is given away. Successful receipt will 297 | return `{:ok, %{kv_set: kv_set, from: from, gift: gift}}` where `from` is the pid of 298 | the previous owner, and `gift` is any additional metadata sent with the table. 299 | 300 | A timeout may be given in milliseconds, which will return `{:error, :timeout}` if reached. 301 | 302 | See `give_away/3` for more information. 303 | """ 304 | @spec accept() :: {:ok, KeyValueSet.t(), pid(), any()} | {:error, any()} 305 | def accept(timeout \\ :infinity) do 306 | with {:ok, %{set: set, from: from, gift: gift}} <- Set.accept(timeout), 307 | do: {:ok, %{kv_set: %KeyValueSet{set: set}, from: from, gift: gift}} 308 | end 309 | 310 | delegate_to_set :info, 2, ret: keyword(), second_param_type: boolean() do 311 | "Returns info on set" 312 | end 313 | 314 | delegate_to_set :get_table, 1, ret: ETS.table_reference(), can_raise: false do 315 | "Returns underlying `:ets` table reference" 316 | end 317 | 318 | delegate_to_set(:first, 1, do: "Returns first key in KeyValueSet") 319 | delegate_to_set(:last, 1, do: "Returns last key in KeyValueSet") 320 | delegate_to_set(:next, 2, do: "Returns next key in KeyValueSet") 321 | delegate_to_set(:previous, 2, do: "Returns previous key in KeyValueSet") 322 | delegate_to_set(:has_key, 2, do: "Determines if specified key exists in KeyValueSet") 323 | delegate_to_set(:delete, 1, do: "Deletes KeyValueSet") 324 | delegate_to_set(:to_list, 1, do: "Returns contents of table as a list") 325 | 326 | ### Access behaviour implementation 327 | 328 | @doc false 329 | @doc since: "0.7.0" 330 | @impl true 331 | def fetch(set, key) do 332 | case get(set, key) do 333 | {:ok, result} -> {:ok, result} 334 | _ -> :error 335 | end 336 | end 337 | 338 | @doc false 339 | @doc since: "0.7.0" 340 | @impl true 341 | def get_and_update(set, key, function) do 342 | value = 343 | case fetch(set, key) do 344 | {:ok, value} -> value 345 | _ -> nil 346 | end 347 | 348 | case function.(value) do 349 | :pop -> {value, delete!(set, key)} 350 | {^value, updated} -> {value, put!(set, key, updated)} 351 | end 352 | end 353 | 354 | @doc false 355 | @doc since: "0.7.0" 356 | @impl true 357 | def pop(set, key) do 358 | case get(set, key) do 359 | {:ok, value} -> {value, delete!(set, key)} 360 | _ -> {nil, set} 361 | end 362 | end 363 | 364 | @doc """ 365 | For processes which may receive ownership of a KeyValueSet unexpectedly - either via 366 | `give_away/3` or by being named the KeyValueSet's heir (see `new/1`) - the module should 367 | include at least one `accept` clause. For example, if we want a server to inherit 368 | KeyValueSets after their previous owner dies: 369 | 370 | ``` 371 | defmodule Receiver do 372 | use GenServer 373 | alias ETS.KeyValueSet 374 | require ETS.KeyValueSet 375 | 376 | ... 377 | 378 | KeyValueSet.accept :owner_crashed, kv_set, _from, state do 379 | new_state = Map.update!(state, :crashed_sets, &[kv_set | &1]) 380 | {:noreply, new_state} 381 | end 382 | ``` 383 | 384 | The first argument is a unique identifier which should match either the "heir_data" 385 | in `new/1`, or the "gift" in `give_away/3`. 386 | The other arguments declare the variables which may be used in the `do` block: 387 | the received KeyValueSet, the pid of the previous owner, and the current state of the process. 388 | 389 | The return value should be in the form {:noreply, new_state}, or one of the similar 390 | returns expected by `handle_info`/`handle_cast`. 391 | """ 392 | defmacro accept(id, table, from, state, do: contents) do 393 | quote do 394 | require Base 395 | 396 | Base.accept unquote(id), unquote(table), unquote(from), unquote(state) do 397 | var!(unquote(table)) = KeyValueSet.wrap_existing!(unquote(table)) 398 | unquote(contents) 399 | end 400 | end 401 | end 402 | end 403 | -------------------------------------------------------------------------------- /lib/ets/key_value_set/macros.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.KeyValueSet.Macros do 2 | @moduledoc false 3 | 4 | use ETS.Utils 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | require Logger 9 | import ETS.KeyValueSet.Macros 10 | end 11 | end 12 | 13 | defmacro delegate_to_set(fun_name, arity, opts \\ [], args) 14 | 15 | defmacro delegate_to_set(fun_name, 1 = arity, opts, do: short_desc) do 16 | desc1 = "#{short_desc}. See `ETS.Set.#{fun_name}/#{arity}`." 17 | desc2 = "Same as `#{fun_name}/#{arity}` but unwraps or raises on error." 18 | fun_name_bang = String.to_atom("#{fun_name}!") 19 | 20 | ret = Keyword.get(opts, :ret, quote(do: any())) 21 | 22 | unwrap_and_raise_or_not = 23 | if Keyword.get(opts, :can_raise, true) do 24 | quote(do: :unwrap_or_raise) 25 | else 26 | quote(do: :unwrap) 27 | end 28 | 29 | quote do 30 | alias ETS.KeyValueSet 31 | alias ETS.Set 32 | 33 | @doc unquote(desc1) 34 | @spec unquote(fun_name)(KeyValueSet.t()) :: {:ok, unquote(ret)} | {:error, any()} 35 | def unquote(fun_name)(%KeyValueSet{set: set}), do: Set.unquote(fun_name)(set) 36 | 37 | @doc unquote(desc2) 38 | @spec unquote(fun_name_bang)(KeyValueSet.t()) :: unquote(ret) 39 | def unquote(fun_name_bang)(%KeyValueSet{} = key_value_set), 40 | do: unquote(unwrap_and_raise_or_not)(KeyValueSet.unquote(fun_name)(key_value_set)) 41 | end 42 | end 43 | 44 | defmacro delegate_to_set(fun_name, 2 = arity, opts, do: short_desc) do 45 | desc1 = "#{short_desc}. See `ETS.Set.#{fun_name}/#{arity}`." 46 | desc2 = "Same as `#{fun_name}/#{arity}` but unwraps or raises on error." 47 | fun_name_bang = String.to_atom("#{fun_name}!") 48 | 49 | ret = Keyword.get(opts, :ret, quote(do: any())) 50 | second_param_type = Keyword.get(opts, :second_param_type, quote(do: any())) 51 | 52 | unwrap_and_raise_or_not = 53 | if Keyword.get(opts, :can_raise, true) do 54 | quote(do: :unwrap_or_raise) 55 | else 56 | quote(do: :unwrap) 57 | end 58 | 59 | quote do 60 | alias ETS.KeyValueSet 61 | alias ETS.Set 62 | 63 | @doc unquote(desc1) 64 | @spec unquote(fun_name)(KeyValueSet.t(), unquote(second_param_type)) :: 65 | {:ok, unquote(ret)} | {:error, any()} 66 | def unquote(fun_name)(%KeyValueSet{set: set}, key), do: Set.unquote(fun_name)(set, key) 67 | 68 | @doc unquote(desc2) 69 | @spec unquote(fun_name_bang)(KeyValueSet.t(), unquote(second_param_type)) :: unquote(ret) 70 | def unquote(fun_name_bang)(%KeyValueSet{} = key_value_set, key), 71 | do: unquote(unwrap_and_raise_or_not)(KeyValueSet.unquote(fun_name)(key_value_set, key)) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/ets/set.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.Set do 2 | @moduledoc """ 3 | Module for creating and interacting with :ets tables of the type `:set` and `:ordered_set`. 4 | 5 | Sets contain "records" which are tuples. Sets are configured with a key position via the `keypos: integer` option. 6 | If not specified, the default key position is 1. The element of the tuple record at the key position is that records key. 7 | For example, setting the `keypos` to 2 means the key of an inserted record `{:a, :b}` is `:b`: 8 | 9 | iex> {:ok, set} = Set.new(keypos: 2) 10 | iex> Set.put!(set, {:a, :b}) 11 | iex> Set.get(set, :a) 12 | {:ok, nil} 13 | iex> Set.get(set, :b) 14 | {:ok, {:a, :b}} 15 | 16 | When a record is added to the table with `put`, it will overwrite an existing record 17 | with the same key. `put_new` will only put the record if a matching key doesn't already exist. 18 | 19 | ## Examples 20 | 21 | iex> {:ok, set} = Set.new(ordered: true) 22 | iex> Set.put_new!(set, {:a, :b, :c}) 23 | iex> Set.to_list!(set) 24 | [{:a, :b, :c}] 25 | iex> Set.put_new!(set, {:d, :e, :f}) 26 | iex> Set.to_list!(set) 27 | [{:a, :b, :c}, {:d, :e, :f}] 28 | iex> Set.put_new!(set, {:a, :g, :h}) 29 | iex> Set.to_list!(set) 30 | [{:a, :b, :c}, {:d, :e, :f}] 31 | 32 | `put` and `put_new` take either a single tuple or a list of tuples. When inserting multiple records, 33 | they are inserted in an atomic an isolated manner. `put_new` doesn't insert any records if any of 34 | the new keys already exist in the set. 35 | 36 | To make your set ordered (which maps to the `:ets` table type `:ordered_set`), specify `ordered: true` 37 | in the options list. An ordered set will store records in term order of the key of the record. This is 38 | helpful when using things like `first`, `last`, `previous`, `next`, and `to_list`, but comes with the penalty of 39 | log(n) insert time vs consistent insert time of an unordered set. 40 | 41 | ## Working with named tables 42 | 43 | The functions on `ETS.Set` require that you pass in an `ETS.Set` as the first argument. In some design patterns, 44 | you may have the table name but an instance of an `ETS.Set` may not be available to you. If this is the case, 45 | you should use `wrap_existing/1` to turn your table name atom into an `ETS.Set`. For example, a `GenServer` that 46 | handles writes within the server, but reads in the client process would be implemented like this: 47 | 48 | ``` 49 | defmodule MyExampleGenServer do 50 | use GenServer 51 | 52 | # Client Functions 53 | 54 | def get_token_for_user(user_id) do 55 | :my_token_table 56 | |> ETS.Set.wrap_existing!() 57 | |> ETS.Set.get!(user_id) 58 | |> elem(1) 59 | end 60 | 61 | def set_token_for_user(user_id, token) do 62 | GenServer.call(__MODULE__, {:set_token_for_user, user_id, token}) 63 | end 64 | 65 | # Server Functions 66 | 67 | def init(_) do 68 | {:ok, %{set: ETS.Set.new!(name: :my_token_table)}} 69 | end 70 | 71 | def handle_call({:set_token_for_user, user_id, token}, _from, %{set: set}) do 72 | ETS.Set.put(set, user_id, token) 73 | end 74 | end 75 | 76 | ``` 77 | 78 | """ 79 | use ETS.Utils 80 | 81 | alias ETS.Base 82 | alias ETS.Set 83 | 84 | @type t :: %__MODULE__{ 85 | info: keyword(), 86 | ordered: boolean(), 87 | table: ETS.table_reference() 88 | } 89 | 90 | @type set_options :: [ETS.Base.option() | {:ordered, boolean()}] 91 | 92 | defstruct table: nil, info: nil, ordered: nil 93 | 94 | @doc """ 95 | Creates new set module with the specified options. 96 | 97 | Note that the underlying :ets table will be attached to the process that calls `new` and will be destroyed 98 | if that process dies. 99 | 100 | Possible options: 101 | 102 | * `name:` when specified, creates a named table with the specified name 103 | * `ordered:` when true, records in set are ordered (default false) 104 | * `protection:` :private, :protected, :public (default :protected) 105 | * `heir:` :none | {heir_pid, heir_data} (default :none) 106 | * `keypos:` integer (default 1) 107 | * `read_concurrency:` boolean (default false) 108 | * `write_concurrency:` boolean (default false) 109 | * `compressed:` boolean (default false) 110 | 111 | ## Examples 112 | 113 | iex> {:ok, set} = Set.new(ordered: true, keypos: 3, read_concurrency: true, compressed: false) 114 | iex> Set.info!(set)[:read_concurrency] 115 | true 116 | 117 | # Named :ets tables via the name keyword 118 | iex> {:ok, set} = Set.new(name: :my_ets_table) 119 | iex> Set.info!(set)[:name] 120 | :my_ets_table 121 | 122 | """ 123 | @spec new(set_options) :: {:error, any()} | {:ok, Set.t()} 124 | def new(opts \\ []) when is_list(opts) do 125 | {opts, ordered} = take_opt(opts, :ordered, false) 126 | 127 | if is_boolean(ordered) do 128 | case Base.new_table(type(ordered), opts) do 129 | {:error, reason} -> {:error, reason} 130 | {:ok, {table, info}} -> {:ok, %Set{table: table, info: info, ordered: ordered}} 131 | end 132 | else 133 | {:error, {:invalid_option, {:ordered, ordered}}} 134 | end 135 | end 136 | 137 | @doc """ 138 | Same as `new/1` but unwraps or raises on error. 139 | """ 140 | @spec new!(set_options) :: Set.t() 141 | def new!(opts \\ []), do: unwrap_or_raise(new(opts)) 142 | 143 | defp type(true), do: :ordered_set 144 | defp type(false), do: :set 145 | 146 | @doc """ 147 | Returns information on the set. 148 | 149 | Second parameter forces updated information from ets, default (false) uses in-struct cached information. 150 | Force should be used when requesting size and memory. 151 | 152 | ## Examples 153 | 154 | iex> {:ok, set} = Set.new(ordered: true, keypos: 3, read_concurrency: true, compressed: false) 155 | iex> {:ok, info} = Set.info(set) 156 | iex> info[:read_concurrency] 157 | true 158 | iex> {:ok, _} = Set.put(set, {:a, :b, :c}) 159 | iex> {:ok, info} = Set.info(set) 160 | iex> info[:size] 161 | 0 162 | iex> {:ok, info} = Set.info(set, true) 163 | iex> info[:size] 164 | 1 165 | 166 | """ 167 | @spec info(Set.t(), boolean()) :: {:ok, keyword()} | {:error, any()} 168 | def info(set, force_update \\ false) 169 | def info(%Set{table: table}, true), do: Base.info(table) 170 | def info(%Set{info: info}, false), do: {:ok, info} 171 | 172 | @doc """ 173 | Same as `info/1` but unwraps or raises on error. 174 | """ 175 | @spec info!(Set.t(), boolean()) :: keyword() 176 | def info!(%Set{} = set, force_update \\ false) when is_boolean(force_update), 177 | do: unwrap_or_raise(info(set, force_update)) 178 | 179 | @doc """ 180 | Returns underlying `:ets` table reference. 181 | 182 | For use in functions that are not yet implemented. If you find yourself using this, please consider 183 | submitting a PR to add the necessary function to `ETS`. 184 | 185 | ## Examples 186 | 187 | iex> set = Set.new!(name: :my_ets_table) 188 | iex> {:ok, table} = Set.get_table(set) 189 | iex> info = :ets.info(table) 190 | iex> info[:name] 191 | :my_ets_table 192 | 193 | """ 194 | @spec get_table(Set.t()) :: {:ok, ETS.table_reference()} 195 | def get_table(%Set{table: table}), do: {:ok, table} 196 | 197 | @doc """ 198 | Same as `get_table/1` but unwraps or raises on error 199 | """ 200 | @spec get_table!(Set.t()) :: ETS.table_reference() 201 | def get_table!(%Set{} = set), do: unwrap(get_table(set)) 202 | 203 | @doc """ 204 | Puts tuple record or list of tuple records into table. Overwrites records for existing key(s). 205 | 206 | Inserts multiple records in an [atomic and isolated](http://erlang.org/doc/man/ets.html#concurrency) manner. 207 | 208 | ## Examples 209 | 210 | iex> {:ok, set} = Set.new(ordered: true) 211 | iex> {:ok, _} = Set.put(set, [{:a, :b, :c}, {:d, :e, :f}]) 212 | iex> {:ok, _} = Set.put(set, {:g, :h, :i}) 213 | iex> {:ok, _} = Set.put(set, {:d, :x, :y}) 214 | iex> Set.to_list(set) 215 | {:ok, [{:a, :b, :c}, {:d, :x, :y}, {:g, :h, :i}]} 216 | 217 | """ 218 | @spec put(Set.t(), tuple() | list(tuple())) :: {:ok, Set.t()} | {:error, any()} 219 | def put(%Set{table: table} = set, record) when is_tuple(record), 220 | do: Base.insert(table, record, set) 221 | 222 | def put(%Set{table: table} = set, records) when is_list(records), 223 | do: Base.insert_multi(table, records, set) 224 | 225 | @doc """ 226 | Same as `put/2` but unwraps or raises on error. 227 | """ 228 | @spec put!(Set.t(), tuple() | list(tuple())) :: Set.t() 229 | def put!(%Set{} = set, record_or_records) 230 | when is_tuple(record_or_records) or is_list(record_or_records), 231 | do: unwrap_or_raise(put(set, record_or_records)) 232 | 233 | @doc """ 234 | Same as `put/2` but doesn't put any records if one of the given keys already exists. 235 | 236 | ## Examples 237 | 238 | iex> set = Set.new!(ordered: true) 239 | iex> {:ok, _} = Set.put_new(set, [{:a, :b, :c}, {:d, :e, :f}]) 240 | iex> {:ok, _} = Set.put_new(set, [{:a, :x, :y}, {:g, :h, :i}]) # skips due to duplicate :a key 241 | iex> {:ok, _} = Set.put_new(set, {:d, :z, :zz}) # skips due to duplicate :d key 242 | iex> Set.to_list!(set) 243 | [{:a, :b, :c}, {:d, :e, :f}] 244 | 245 | """ 246 | @spec put_new(Set.t(), tuple() | list(tuple())) :: {:ok, Set.t()} | {:error, any()} 247 | def put_new(%Set{table: table} = set, record) when is_tuple(record), 248 | do: Base.insert_new(table, record, set) 249 | 250 | def put_new(%Set{table: table} = set, records) when is_list(records), 251 | do: Base.insert_multi_new(table, records, set) 252 | 253 | @doc """ 254 | Same as `put_new/2` but unwraps or raises on error. 255 | """ 256 | @spec put_new!(Set.t(), tuple() | list(tuple())) :: Set.t() 257 | def put_new!(%Set{} = set, record_or_records) 258 | when is_tuple(record_or_records) or is_list(record_or_records), 259 | do: unwrap_or_raise(put_new(set, record_or_records)) 260 | 261 | @doc """ 262 | Returns record with specified key or an error if no record found. 263 | 264 | ## Examples 265 | 266 | iex> Set.new!() 267 | iex> |> Set.put!({:a, :b, :c}) 268 | iex> |> Set.put!({:d, :e, :f}) 269 | iex> |> Set.fetch(:d) 270 | {:ok, {:d, :e, :f}} 271 | 272 | iex> Set.new!() 273 | iex> |> Set.put!({:a, :b, :c}) 274 | iex> |> Set.put!({:d, :e, :f}) 275 | iex> |> Set.fetch(:g) 276 | {:error, :key_not_found} 277 | 278 | """ 279 | @spec fetch(Set.t(), any()) :: {:ok, tuple() | nil} | {:error, any()} 280 | def fetch(%Set{table: table}, key) do 281 | case Base.lookup(table, key) do 282 | {:ok, []} -> {:error, :key_not_found} 283 | {:ok, [x | []]} -> {:ok, x} 284 | {:ok, _} -> {:error, :invalid_set} 285 | {:error, reason} -> {:error, reason} 286 | end 287 | end 288 | 289 | @doc """ 290 | Returns record with specified key or the provided default (nil if not specified) if no record found. 291 | 292 | ## Examples 293 | 294 | iex> Set.new!() 295 | iex> |> Set.put!({:a, :b, :c}) 296 | iex> |> Set.put!({:d, :e, :f}) 297 | iex> |> Set.get(:d) 298 | {:ok, {:d, :e, :f}} 299 | 300 | """ 301 | @spec get(Set.t(), any(), any()) :: {:ok, tuple() | nil} | {:error, any()} 302 | def get(%Set{table: table}, key, default \\ nil) do 303 | case Base.lookup(table, key) do 304 | {:ok, []} -> {:ok, default} 305 | {:ok, [x | []]} -> {:ok, x} 306 | {:ok, _} -> {:error, :invalid_set} 307 | {:error, reason} -> {:error, reason} 308 | end 309 | end 310 | 311 | @doc """ 312 | Same as `get/3` but unwraps or raises on error. 313 | """ 314 | @spec get!(Set.t(), any(), any()) :: tuple() | nil 315 | def get!(%Set{} = set, key, default \\ nil), do: unwrap_or_raise(get(set, key, default)) 316 | 317 | @doc """ 318 | Returns element in specified position of record with specified key. 319 | 320 | ## Examples 321 | 322 | iex> Set.new!() 323 | iex> |> Set.put!({:a, :b, :c}) 324 | iex> |> Set.put!({:d, :e, :f}) 325 | iex> |> Set.get_element(:d, 2) 326 | {:ok, :e} 327 | 328 | """ 329 | @spec get_element(Set.t(), any(), non_neg_integer()) :: {:ok, any()} | {:error, any()} 330 | def get_element(%Set{table: table}, key, pos), do: Base.lookup_element(table, key, pos) 331 | 332 | @doc """ 333 | Same as `get_element/3` but unwraps or raises on error. 334 | """ 335 | @spec get_element!(Set.t(), any(), non_neg_integer()) :: any() 336 | def get_element!(%Set{} = set, key, pos), do: unwrap_or_raise(get_element(set, key, pos)) 337 | 338 | @doc """ 339 | Returns records in the specified Set that match the specified pattern. 340 | 341 | For more information on the match pattern, see the [erlang documentation](http://erlang.org/doc/man/ets.html#match-2) 342 | 343 | ## Examples 344 | 345 | iex> Set.new!(ordered: true) 346 | iex> |> Set.put!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 347 | iex> |> Set.match({:"$1", :b, :"$2", :_}) 348 | {:ok, [[:a, :c], [:h, :i]]} 349 | 350 | """ 351 | @spec match(Set.t(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()} 352 | def match(%Set{table: table}, pattern) when is_atom(pattern) or is_tuple(pattern), 353 | do: Base.match(table, pattern) 354 | 355 | @doc """ 356 | Same as `match/2` but unwraps or raises on error. 357 | """ 358 | @spec match!(Set.t(), ETS.match_pattern()) :: [tuple()] 359 | def match!(%Set{} = set, pattern) when is_atom(pattern) or is_tuple(pattern), 360 | do: unwrap_or_raise(match(set, pattern)) 361 | 362 | @doc """ 363 | Same as `match/2` but limits number of results to the specified limit. 364 | 365 | ## Examples 366 | 367 | iex> set = Set.new!(ordered: true) 368 | iex> Set.put!(set, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 369 | iex> {:ok, {results, _continuation}} = Set.match(set, {:"$1", :b, :"$2", :_}, 2) 370 | iex> results 371 | [[:a, :c], [:e, :f]] 372 | 373 | """ 374 | @spec match(Set.t(), ETS.match_pattern(), non_neg_integer()) :: 375 | {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 376 | def match(%Set{table: table}, pattern, limit), do: Base.match(table, pattern, limit) 377 | 378 | @doc """ 379 | Same as `match/3` but unwraps or raises on error. 380 | """ 381 | @spec match!(Set.t(), ETS.match_pattern(), non_neg_integer()) :: 382 | {[tuple()], any() | :end_of_table} 383 | def match!(%Set{} = set, pattern, limit), do: unwrap_or_raise(match(set, pattern, limit)) 384 | 385 | @doc """ 386 | Matches next set of records from a match/3 or match/1 continuation. 387 | 388 | ## Examples 389 | 390 | iex> set = Set.new!(ordered: true) 391 | iex> Set.put!(set, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 392 | iex> {:ok, {results, continuation}} = Set.match(set, {:"$1", :b, :"$2", :_}, 2) 393 | iex> results 394 | [[:a, :c], [:e, :f]] 395 | iex> {:ok, {records2, continuation2}} = Set.match(continuation) 396 | iex> records2 397 | [[:h, :i]] 398 | iex> continuation2 399 | :end_of_table 400 | 401 | """ 402 | @spec match(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 403 | def match(continuation), do: Base.match(continuation) 404 | 405 | @doc """ 406 | Same as `match/1` but unwraps or raises on error. 407 | """ 408 | @spec match!(any()) :: {[tuple()], any() | :end_of_table} 409 | def match!(continuation), do: unwrap_or_raise(match(continuation)) 410 | 411 | @doc """ 412 | Deletes all records that match the specified pattern. 413 | 414 | Always returns `:ok`, regardless of whether anything was deleted or not. 415 | 416 | ## Examples 417 | 418 | iex> set = Set.new!(ordered: true) 419 | iex> Set.put!(set, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :i, :j, :k}]) 420 | iex> Set.match_delete(set, {:_, :b, :_, :_}) 421 | {:ok, set} 422 | iex> Set.to_list!(set) 423 | [{:h, :i, :j, :k}] 424 | 425 | """ 426 | @spec match_delete(Set.t(), ETS.match_pattern()) :: {:ok, Set.t()} | {:error, any()} 427 | def match_delete(%Set{table: table} = set, pattern) 428 | when is_atom(pattern) or is_tuple(pattern) do 429 | with :ok <- Base.match_delete(table, pattern) do 430 | {:ok, set} 431 | end 432 | end 433 | 434 | @doc """ 435 | Same as `match_delete/2` but unwraps or raises on error. 436 | """ 437 | @spec match_delete!(Set.t(), ETS.match_pattern()) :: Set.t() 438 | def match_delete!(%Set{} = set, pattern) when is_atom(pattern) or is_tuple(pattern), 439 | do: unwrap_or_raise(match_delete(set, pattern)) 440 | 441 | @doc """ 442 | Returns records in the specified Set that match the specified pattern. 443 | 444 | For more information on the match pattern, see the [erlang documentation](http://erlang.org/doc/man/ets.html#match-2) 445 | 446 | ## Examples 447 | 448 | iex> Set.new!(ordered: true) 449 | iex> |> Set.put!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 450 | iex> |> Set.match_object({:"$1", :b, :"$2", :_}) 451 | {:ok, [{:a, :b, :c, :d}, {:h, :b, :i, :j}]} 452 | 453 | """ 454 | @spec match_object(Set.t(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()} 455 | def match_object(%Set{table: table}, pattern) when is_atom(pattern) or is_tuple(pattern), 456 | do: Base.match_object(table, pattern) 457 | 458 | @doc """ 459 | Same as `match_object/2` but unwraps or raises on error. 460 | """ 461 | @spec match_object!(Set.t(), ETS.match_pattern()) :: [tuple()] 462 | def match_object!(%Set{} = set, pattern) when is_atom(pattern) or is_tuple(pattern), 463 | do: unwrap_or_raise(match_object(set, pattern)) 464 | 465 | @doc """ 466 | Same as `match_object/2` but limits number of results to the specified limit. 467 | 468 | ## Examples 469 | 470 | iex> set = Set.new!(ordered: true) 471 | iex> Set.put!(set, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 472 | iex> {:ok, {results, _continuation}} = Set.match_object(set, {:"$1", :b, :"$2", :_}, 2) 473 | iex> results 474 | [{:a, :b, :c, :d}, {:e, :b, :f, :g}] 475 | 476 | """ 477 | @spec match_object(Set.t(), ETS.match_pattern(), non_neg_integer()) :: 478 | {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 479 | def match_object(%Set{table: table}, pattern, limit), 480 | do: Base.match_object(table, pattern, limit) 481 | 482 | @doc """ 483 | Same as `match_object/3` but unwraps or raises on error. 484 | """ 485 | @spec match_object!(Set.t(), ETS.match_pattern(), non_neg_integer()) :: 486 | {[tuple()], any() | :end_of_table} 487 | def match_object!(%Set{} = set, pattern, limit), 488 | do: unwrap_or_raise(match_object(set, pattern, limit)) 489 | 490 | @doc """ 491 | Matches next records from a match_object/3 or match_object/1 continuation. 492 | 493 | ## Examples 494 | 495 | iex> set = Set.new!(ordered: true) 496 | iex> Set.put!(set, [{:a, :b, :c}, {:d, :b, :e}, {:f, :b, :g}, {:h, :b, :i}]) 497 | iex> {:ok, {results, continuation}} = Set.match_object(set, {:"$1", :b, :_}, 2) 498 | iex> results 499 | [{:a, :b, :c}, {:d, :b, :e}] 500 | iex> {:ok, {results2, continuation2}} = Set.match_object(continuation) 501 | iex> results2 502 | [{:f, :b, :g}, {:h, :b, :i}] 503 | iex> {:ok, {[], :end_of_table}} = Set.match_object(continuation2) 504 | 505 | """ 506 | @spec match_object(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()} 507 | def match_object(continuation), do: Base.match_object(continuation) 508 | 509 | @doc """ 510 | Same as `match_object/1` but unwraps or raises on error. 511 | """ 512 | @spec match_object!(any()) :: {[tuple()], any() | :end_of_table} 513 | def match_object!(continuation), do: unwrap_or_raise(match_object(continuation)) 514 | 515 | @spec select(ETS.continuation()) :: 516 | {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()} 517 | def select(continuation), do: Base.select(continuation) 518 | 519 | @spec select!(ETS.continuation()) :: {[tuple()], ETS.continuation()} | ETS.end_of_table() 520 | def select!(continuation) do 521 | unwrap_or_raise(select(continuation)) 522 | end 523 | 524 | @doc """ 525 | Returns records in the specified Set that match the specified match specification. 526 | 527 | For more information on the match specification, see the [erlang documentation](http://erlang.org/doc/man/ets.html#select-2) 528 | 529 | ## Examples 530 | 531 | iex> Set.new!(ordered: true) 532 | iex> |> Set.put!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :i, :j}]) 533 | iex> |> Set.select([{{:"$1", :b, :"$2", :_},[],[:"$$"]}]) 534 | {:ok, [[:a, :c], [:h, :i]]} 535 | 536 | """ 537 | @spec select(Set.t(), ETS.match_spec()) :: {:ok, [tuple()]} | {:error, any()} 538 | def select(%Set{table: table}, spec) when is_list(spec), 539 | do: Base.select(table, spec) 540 | 541 | @doc """ 542 | Same as `select/2` but unwraps or raises on error. 543 | """ 544 | @spec select!(Set.t(), ETS.match_spec()) :: [tuple()] 545 | def select!(%Set{} = set, spec) when is_list(spec), 546 | do: unwrap_or_raise(select(set, spec)) 547 | 548 | @doc """ 549 | Same as `select/2` but limits the number of results returned. 550 | """ 551 | @spec select(Set.t(), ETS.match_spec(), limit :: integer) :: 552 | {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()} 553 | def select(%Set{table: table}, spec, limit) when is_list(spec), 554 | do: Base.select(table, spec, limit) 555 | 556 | @doc """ 557 | Same as `select/3` but unwraps or raises on error. 558 | """ 559 | @spec select!(Set.t(), ETS.match_spec(), limit :: integer) :: 560 | {[tuple()], ETS.continuation()} | ETS.end_of_table() 561 | def select!(%Set{} = set, spec, limit) when is_list(spec), 562 | do: unwrap_or_raise(select(set, spec, limit)) 563 | 564 | @doc """ 565 | Deletes records in the specified Set that match the specified match specification. 566 | 567 | For more information on the match specification, see the [erlang documentation](http://erlang.org/doc/man/ets.html#select_delete-2) 568 | 569 | ## Examples 570 | 571 | iex> set = Set.new!(ordered: true) 572 | iex> set 573 | iex> |> Set.put!([{:a, :b, :c, :d}, {:e, :c, :f, :g}, {:h, :b, :c, :h}]) 574 | iex> |> Set.select_delete([{{:"$1", :b, :"$2", :_},[{:"==", :"$2", :c}],[true]}]) 575 | {:ok, 2} 576 | iex> Set.to_list!(set) 577 | [{:e, :c, :f, :g}] 578 | 579 | """ 580 | @spec select_delete(Set.t(), ETS.match_spec()) :: {:ok, non_neg_integer()} | {:error, any()} 581 | def select_delete(%Set{table: table}, spec) when is_list(spec), 582 | do: Base.select_delete(table, spec) 583 | 584 | @doc """ 585 | Same as `select_delete/2` but unwraps or raises on error. 586 | """ 587 | @spec select_delete!(Set.t(), ETS.match_spec()) :: non_neg_integer() 588 | def select_delete!(%Set{} = set, spec) when is_list(spec), 589 | do: unwrap_or_raise(select_delete(set, spec)) 590 | 591 | @doc """ 592 | Determines if specified key exists in specified set. 593 | 594 | ## Examples 595 | 596 | iex> set = Set.new!() 597 | iex> Set.has_key(set, :key) 598 | {:ok, false} 599 | iex> Set.put(set, {:key, :value}) 600 | iex> Set.has_key(set, :key) 601 | {:ok, true} 602 | 603 | """ 604 | @spec has_key(Set.t(), any()) :: {:ok, boolean()} | {:error, any()} 605 | def has_key(%Set{table: table}, key), do: Base.has_key(table, key) 606 | 607 | @doc """ 608 | Same as `has_key/2` but unwraps or raises on error. 609 | """ 610 | @spec has_key!(Set.t(), any()) :: boolean() 611 | def has_key!(set, key), do: unwrap_or_raise(has_key(set, key)) 612 | 613 | @doc """ 614 | Returns the first key in the specified Set. Set must be ordered or error is returned. 615 | 616 | ## Examples 617 | 618 | iex> set = Set.new!(ordered: true) 619 | iex> Set.first(set) 620 | {:error, :empty_table} 621 | iex> Set.put!(set, {:key1, :val}) 622 | iex> Set.put!(set, {:key2, :val}) 623 | iex> Set.first(set) 624 | {:ok, :key1} 625 | 626 | """ 627 | @spec first(Set.t()) :: {:ok, any()} | {:error, any()} 628 | def first(%Set{ordered: false}), do: {:error, :set_not_ordered} 629 | def first(%Set{table: table}), do: Base.first(table) 630 | 631 | @doc """ 632 | Same as `first/1` but unwraps or raises on error 633 | """ 634 | @spec first!(Set.t()) :: any() 635 | def first!(%Set{} = set), do: unwrap_or_raise(first(set)) 636 | 637 | @doc """ 638 | Returns the last key in the specified Set. Set must be ordered or error is returned. 639 | 640 | ## Examples 641 | 642 | iex> set = Set.new!(ordered: true) 643 | iex> Set.last(set) 644 | {:error, :empty_table} 645 | iex> Set.put!(set, {:key1, :val}) 646 | iex> Set.put!(set, {:key2, :val}) 647 | iex> Set.last(set) 648 | {:ok, :key2} 649 | 650 | """ 651 | @spec last(Set.t()) :: {:ok, any()} | {:error, any()} 652 | def last(%Set{ordered: false}), do: {:error, :set_not_ordered} 653 | def last(%Set{table: table}), do: Base.last(table) 654 | 655 | @doc """ 656 | Same as `last/1` but unwraps or raises on error 657 | """ 658 | @spec last!(Set.t()) :: any() 659 | def last!(set), do: unwrap_or_raise(last(set)) 660 | 661 | @doc """ 662 | Returns the next key in the specified Set. 663 | 664 | The given key does not need to exist in the set. The key returned will be the first key that exists in the 665 | set which is subsequent in term order to the key given. 666 | 667 | Set must be ordered or error is returned. 668 | 669 | ## Examples 670 | 671 | iex> set = Set.new!(ordered: true) 672 | iex> Set.put!(set, {:key1, :val}) 673 | iex> Set.put!(set, {:key2, :val}) 674 | iex> Set.put!(set, {:key3, :val}) 675 | iex> Set.first(set) 676 | {:ok, :key1} 677 | iex> Set.next(set, :key1) 678 | {:ok, :key2} 679 | iex> Set.next(set, :key2) 680 | {:ok, :key3} 681 | iex> Set.next(set, :key3) 682 | {:error, :end_of_table} 683 | iex> Set.next(set, :a) 684 | {:ok, :key1} 685 | iex> Set.next(set, :z) 686 | {:error, :end_of_table} 687 | 688 | """ 689 | @spec next(Set.t(), any()) :: {:ok, any()} | {:error, any()} 690 | def next(%Set{ordered: false}, _key), do: {:error, :set_not_ordered} 691 | def next(%Set{table: table}, key), do: Base.next(table, key) 692 | 693 | @doc """ 694 | Same as `next/1` but unwraps or raises on error 695 | """ 696 | @spec next!(Set.t(), any()) :: any() 697 | def next!(set, key), do: unwrap_or_raise(next(set, key)) 698 | 699 | @doc """ 700 | Returns the previous key in the specified Set. 701 | 702 | The given key does not need to exist in the set. The key returned will be the first key that exists in the 703 | set which is previous in term order to the key given. 704 | 705 | Set must be ordered or error is returned. 706 | 707 | ## Examples 708 | 709 | iex> set = Set.new!(ordered: true) 710 | iex> Set.put!(set, {:key1, :val}) 711 | iex> Set.put!(set, {:key2, :val}) 712 | iex> Set.put!(set, {:key3, :val}) 713 | iex> Set.last(set) 714 | {:ok, :key3} 715 | iex> Set.previous(set, :key3) 716 | {:ok, :key2} 717 | iex> Set.previous(set, :key2) 718 | {:ok, :key1} 719 | iex> Set.previous(set, :key1) 720 | {:error, :start_of_table} 721 | iex> Set.previous(set, :a) 722 | {:error, :start_of_table} 723 | iex> Set.previous(set, :z) 724 | {:ok, :key3} 725 | 726 | """ 727 | @spec previous(Set.t(), any()) :: {:ok, any()} | {:error, any()} 728 | def previous(%Set{ordered: false}, _key), do: {:error, :set_not_ordered} 729 | 730 | def previous(%Set{table: table}, key), do: Base.previous(table, key) 731 | 732 | @doc """ 733 | Same as `previous/1` but raises on :error 734 | 735 | Returns previous key in table. 736 | """ 737 | @spec previous!(Set.t(), any()) :: any() 738 | def previous!(%Set{} = set, key), do: unwrap_or_raise(previous(set, key)) 739 | 740 | @doc """ 741 | Returns contents of table as a list. 742 | 743 | ## Examples 744 | 745 | iex> Set.new!(ordered: true) 746 | iex> |> Set.put!({:a, :b, :c}) 747 | iex> |> Set.put!({:d, :e, :f}) 748 | iex> |> Set.put!({:d, :e, :f}) 749 | iex> |> Set.to_list() 750 | {:ok, [{:a, :b, :c}, {:d, :e, :f}]} 751 | 752 | """ 753 | @spec to_list(Set.t()) :: {:ok, [tuple()]} | {:error, any()} 754 | def to_list(%Set{table: table}), do: Base.to_list(table) 755 | 756 | @doc """ 757 | Same as `to_list/1` but unwraps or raises on error. 758 | """ 759 | @spec to_list!(Set.t()) :: [tuple()] 760 | def to_list!(%Set{} = set), do: unwrap_or_raise(to_list(set)) 761 | 762 | @doc """ 763 | Deletes specified Set. 764 | 765 | ## Examples 766 | 767 | iex> {:ok, set} = Set.new() 768 | iex> {:ok, _} = Set.info(set, true) 769 | iex> {:ok, _} = Set.delete(set) 770 | iex> Set.info(set, true) 771 | {:error, :table_not_found} 772 | 773 | """ 774 | @spec delete(Set.t()) :: {:ok, Set.t()} | {:error, any()} 775 | def delete(%Set{table: table} = set), do: Base.delete(table, set) 776 | 777 | @doc """ 778 | Same as `delete/1` but unwraps or raises on error. 779 | """ 780 | @spec delete!(Set.t()) :: Set.t() 781 | def delete!(%Set{} = set), do: unwrap_or_raise(delete(set)) 782 | 783 | @doc """ 784 | Deletes record with specified key in specified Set. 785 | 786 | ## Examples 787 | 788 | iex> set = Set.new!() 789 | iex> Set.put(set, {:a, :b, :c}) 790 | iex> Set.delete(set, :a) 791 | iex> Set.get!(set, :a) 792 | nil 793 | 794 | """ 795 | @spec delete(Set.t(), any()) :: {:ok, Set.t()} | {:error, any()} 796 | def delete(%Set{table: table} = set, key), do: Base.delete_records(table, key, set) 797 | 798 | @doc """ 799 | Same as `delete/2` but unwraps or raises on error. 800 | """ 801 | @spec delete!(Set.t(), any()) :: Set.t() 802 | def delete!(%Set{} = set, key), do: unwrap_or_raise(delete(set, key)) 803 | 804 | @doc """ 805 | Deletes all records in specified Set. 806 | 807 | ## Examples 808 | 809 | iex> set = Set.new!() 810 | iex> set 811 | iex> |> Set.put!({:a, :b, :c}) 812 | iex> |> Set.put!({:b, :b, :c}) 813 | iex> |> Set.put!({:c, :b, :c}) 814 | iex> |> Set.to_list!() 815 | [{:c, :b, :c}, {:b, :b, :c}, {:a, :b, :c}] 816 | iex> Set.delete_all(set) 817 | iex> Set.to_list!(set) 818 | [] 819 | 820 | """ 821 | @spec delete_all(Set.t()) :: {:ok, Set.t()} | {:error, any()} 822 | def delete_all(%Set{table: table} = set), do: Base.delete_all_records(table, set) 823 | 824 | @doc """ 825 | Same as `delete_all/1` but unwraps or raises on error. 826 | """ 827 | @spec delete_all!(Set.t()) :: Set.t() 828 | def delete_all!(%Set{} = set), do: unwrap_or_raise(delete_all(set)) 829 | 830 | @doc """ 831 | Updates one or more elements within the record with the given `key`. The element_spec is 832 | a tuple (or list of tuples) of the form `{position, value}`, which will update the element 833 | at `position` (1-indexed) to have the given `value`. When a list is given, multiple elements 834 | can be updated within the same record. If the same position occurs more than once in the list, 835 | the last value in the list is written. If the list is empty or the function fails, no updates 836 | are done. The function is also atomic in the sense that other processes can never see any 837 | intermediate results. 838 | 839 | Returns `{:ok, set}` if an object with the given key is found, otherwise returns 840 | `{:error, :key_not_found}`. 841 | 842 | ## Examples 843 | 844 | iex> set = Set.new!() 845 | iex> Set.put!(set, {:a, :b, :c}) 846 | iex> Set.update_element(set, :a, {2, :d}) 847 | {:ok, set} 848 | iex> Set.to_list!(set) 849 | [{:a, :d, :c}] 850 | iex> Set.update_element(set, :a, [{2, :x}, {3, :y}]) 851 | {:ok, set} 852 | iex> Set.to_list!(set) 853 | [{:a, :x, :y}] 854 | 855 | """ 856 | @spec update_element(Set.t(), any(), tuple() | [tuple()]) :: {:ok, Set.t()} | {:error, any()} 857 | def update_element(%Set{table: table} = set, key, element_spec) do 858 | case Base.update_element(table, key, element_spec) do 859 | true -> {:ok, set} 860 | false -> {:error, :key_not_found} 861 | error -> error 862 | end 863 | end 864 | 865 | @doc """ 866 | Same as `update_element/3` but unwraps or raises on error. 867 | """ 868 | @spec update_element!(Set.t(), any(), tuple() | [tuple()]) :: Set.t() 869 | def update_element!(%Set{} = set, key, element_spec), 870 | do: unwrap_or_raise(update_element(set, key, element_spec)) 871 | 872 | @doc """ 873 | Wraps an existing :ets :set or :ordered_set in a Set struct. 874 | 875 | ## Examples 876 | 877 | iex> :ets.new(:my_ets_table, [:set, :named_table]) 878 | iex> {:ok, set} = Set.wrap_existing(:my_ets_table) 879 | iex> Set.info!(set)[:name] 880 | :my_ets_table 881 | 882 | """ 883 | @spec wrap_existing(ETS.table_identifier()) :: {:ok, Set.t()} | {:error, any()} 884 | def wrap_existing(table_identifier) do 885 | case Base.wrap_existing(table_identifier, [:set, :ordered_set]) do 886 | {:ok, {table, info}} -> 887 | {:ok, %Set{table: table, info: info, ordered: info[:type] == :ordered_set}} 888 | 889 | {:error, reason} -> 890 | {:error, reason} 891 | end 892 | end 893 | 894 | @doc """ 895 | Same as `wrap_existing/1` but unwraps or raises on error. 896 | """ 897 | @spec wrap_existing!(ETS.table_identifier()) :: Set.t() 898 | def wrap_existing!(table_identifier), do: unwrap_or_raise(wrap_existing(table_identifier)) 899 | 900 | @doc """ 901 | Transfers ownership of a Set to another process. 902 | 903 | ## Examples 904 | 905 | iex> set = Set.new!() 906 | iex> receiver_pid = spawn(fn -> Set.accept() end) 907 | iex> Set.give_away(set, receiver_pid) 908 | {:ok, set} 909 | 910 | iex> set = Set.new!() 911 | iex> dead_pid = ETS.TestUtils.dead_pid() 912 | iex> Set.give_away(set, dead_pid) 913 | {:error, :recipient_not_alive} 914 | 915 | """ 916 | @spec give_away(Set.t(), pid(), any()) :: {:ok, Set.t()} | {:error, any()} 917 | def give_away(%Set{table: table} = set, pid, gift \\ []), 918 | do: Base.give_away(table, pid, gift, set) 919 | 920 | @doc """ 921 | Same as `give_away/3` but unwraps or raises on error. 922 | """ 923 | @spec give_away!(Set.t(), pid(), any()) :: Set.t() 924 | def give_away!(%Set{} = set, pid, gift \\ []), 925 | do: unwrap_or_raise(give_away(set, pid, gift)) 926 | 927 | @doc """ 928 | Waits to accept ownership of a table after it is given away. Successful receipt will 929 | return `{:ok, %{set: set, from: from, gift: gift}}` where `from` is the pid of the previous 930 | owner, and `gift` is any additional metadata sent with the table. 931 | 932 | A timeout may be given in milliseconds, which will return `{:error, :timeout}` if reached. 933 | 934 | See `give_away/3` for more information. 935 | """ 936 | @spec accept() :: {:ok, Set.t(), pid(), any()} | {:error, any()} 937 | def accept(timeout \\ :infinity) do 938 | with {:ok, table, from, gift} <- Base.accept(timeout), 939 | {:ok, set} <- Set.wrap_existing(table) do 940 | {:ok, %{set: set, from: from, gift: gift}} 941 | end 942 | end 943 | 944 | @doc """ 945 | For processes which may receive ownership of a Set unexpectedly - either via `give_away/3` or 946 | by being named the Set's heir (see `new/1`) - the module should include at least one `accept` 947 | clause. For example, if we want a server to inherit Sets after their previous owner dies: 948 | 949 | ``` 950 | defmodule Receiver do 951 | use GenServer 952 | alias ETS.Set 953 | require ETS.Set 954 | 955 | ... 956 | 957 | Set.accept :owner_crashed, set, _from, state do 958 | new_state = Map.update!(state, :crashed_sets, &[set | &1]) 959 | {:noreply, new_state} 960 | end 961 | ``` 962 | 963 | The first argument is a unique identifier which should match either the "heir_data" 964 | in `new/1`, or the "gift" in `give_away/3`. 965 | The other arguments declare the variables which may be used in the `do` block: 966 | the received Set, the pid of the previous owner, and the current state of the process. 967 | 968 | The return value should be in the form {:noreply, new_state}, or one of the similar 969 | returns expected by `handle_info`/`handle_cast`. 970 | """ 971 | defmacro accept(id, table, from, state, do: contents) do 972 | quote do 973 | require Base 974 | 975 | Base.accept unquote(id), unquote(table), unquote(from), unquote(state) do 976 | var!(unquote(table)) = Set.wrap_existing!(unquote(table)) 977 | unquote(contents) 978 | end 979 | end 980 | end 981 | end 982 | -------------------------------------------------------------------------------- /lib/ets/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.Utils do 2 | @moduledoc """ 3 | Contains helper macros used by `ETS` modules. 4 | """ 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | require Logger 9 | import ETS.Utils 10 | end 11 | end 12 | 13 | def take_opt(opts, key, default) do 14 | val = Keyword.get(opts, key, default) 15 | {Keyword.drop(opts, [key]), val} 16 | end 17 | 18 | defmacro catch_error(do: do_block) do 19 | {func, arity} = __CALLER__.function 20 | mod = __CALLER__.module 21 | 22 | quote do 23 | try do 24 | unquote(do_block) 25 | rescue 26 | e in ArgumentError -> 27 | Logger.error( 28 | "Unknown ArgumentError in #{inspect(unquote(mod))}.#{unquote(func)}/#{unquote(arity)}: #{inspect(e)}" 29 | ) 30 | 31 | {:error, :unknown_error} 32 | end 33 | end 34 | end 35 | 36 | defmacro catch_table_not_found(table, do: do_block) do 37 | quote do 38 | try do 39 | unquote(do_block) 40 | rescue 41 | e in ArgumentError -> 42 | case :ets.info(unquote(table)) do 43 | :undefined -> {:error, :table_not_found} 44 | _ -> reraise(e, __STACKTRACE__) 45 | end 46 | end 47 | end 48 | end 49 | 50 | defmacro catch_table_already_exists(table_name, do: do_block) do 51 | quote do 52 | try do 53 | unquote(do_block) 54 | rescue 55 | e in ArgumentError -> 56 | case :ets.whereis(unquote(table_name)) do 57 | :undefined -> reraise(e, __STACKTRACE__) 58 | _ -> {:error, :table_already_exists} 59 | end 60 | end 61 | end 62 | end 63 | 64 | defmacro catch_key_not_found(table, key, do: do_block) do 65 | quote do 66 | try do 67 | unquote(do_block) 68 | rescue 69 | e in ArgumentError -> 70 | case ETS.Base.lookup(unquote(table), unquote(key)) do 71 | {:ok, []} -> {:error, :key_not_found} 72 | _ -> reraise(e, __STACKTRACE__) 73 | end 74 | end 75 | end 76 | end 77 | 78 | defmacro catch_bad_records(records, do: do_block) do 79 | quote do 80 | try do 81 | unquote(do_block) 82 | rescue 83 | e in ArgumentError -> 84 | if Enum.any?(unquote(records), &(!is_tuple(&1))) do 85 | {:error, :invalid_record} 86 | else 87 | reraise(e, __STACKTRACE__) 88 | end 89 | end 90 | end 91 | end 92 | 93 | defmacro catch_record_too_small(table, record, do: do_block) do 94 | quote do 95 | try do 96 | unquote(do_block) 97 | rescue 98 | e in ArgumentError -> 99 | if :ets.info(unquote(table))[:keypos] > tuple_size(unquote(record)) do 100 | {:error, :record_too_small} 101 | else 102 | reraise(e, __STACKTRACE__) 103 | end 104 | end 105 | end 106 | end 107 | 108 | defmacro catch_records_too_small(table, records, do: do_block) do 109 | quote do 110 | try do 111 | unquote(do_block) 112 | rescue 113 | e in ArgumentError -> 114 | keypos = :ets.info(unquote(table))[:keypos] 115 | 116 | unquote(records) 117 | |> Enum.filter(&(keypos > tuple_size(&1))) 118 | |> case do 119 | [] -> reraise(e, __STACKTRACE__) 120 | _ -> {:error, :record_too_small} 121 | end 122 | end 123 | end 124 | end 125 | 126 | defmacro catch_position_out_of_bounds(table, key, pos, do: do_block) do 127 | quote do 128 | try do 129 | unquote(do_block) 130 | rescue 131 | e in ArgumentError -> 132 | unquote(table) 133 | |> :ets.lookup(unquote(key)) 134 | |> Enum.any?(&(tuple_size(&1) < unquote(pos))) 135 | |> if do 136 | {:error, :position_out_of_bounds} 137 | else 138 | reraise(e, __STACKTRACE__) 139 | end 140 | end 141 | end 142 | end 143 | 144 | defmacro catch_positions_out_of_bounds(table, key, element_spec, do: do_block) do 145 | quote do 146 | try do 147 | unquote(do_block) 148 | rescue 149 | e in ArgumentError -> 150 | [result] = :ets.lookup(unquote(table), unquote(key)) 151 | size = tuple_size(result) 152 | specs = List.wrap(unquote(element_spec)) 153 | 154 | if Enum.any?(specs, fn {pos, _} -> pos < 1 or pos > size end) do 155 | {:error, :position_out_of_bounds} 156 | else 157 | reraise(e, __STACKTRACE__) 158 | end 159 | end 160 | end 161 | end 162 | 163 | defmacro catch_key_update(table, element_spec, do: do_block) do 164 | quote do 165 | try do 166 | unquote(do_block) 167 | rescue 168 | e in ArgumentError -> 169 | info = :ets.info(unquote(table)) 170 | specs = List.wrap(unquote(element_spec)) 171 | 172 | if Enum.any?(specs, fn {pos, _} -> pos == info[:keypos] end) do 173 | {:error, :cannot_update_key} 174 | else 175 | reraise(e, __STACKTRACE__) 176 | end 177 | end 178 | end 179 | end 180 | 181 | defmacro catch_invalid_select_spec(spec, do: do_block) do 182 | quote do 183 | try do 184 | unquote(do_block) 185 | rescue 186 | e in ArgumentError -> 187 | if ETS.Utils.valid_select_spec?(unquote(spec)) do 188 | reraise(e, __STACKTRACE__) 189 | else 190 | {:error, :invalid_select_spec} 191 | end 192 | end 193 | end 194 | end 195 | 196 | def valid_select_spec?(spec) when is_list(spec) do 197 | spec 198 | |> Enum.all?(fn s -> 199 | s |> is_tuple() and 200 | s |> tuple_size() == 3 and 201 | s |> elem(0) |> is_tuple() and 202 | s |> elem(1) |> is_list() and 203 | s |> elem(2) |> is_list() 204 | end) 205 | end 206 | 207 | def valid_select_spec?(_not_a_list), do: false 208 | 209 | def continuation_table(:"$end_of_table"), do: {:ok, :"$end_of_table"} 210 | 211 | def continuation_table({table, i1, i2, _match_spec, list, i3}) 212 | when is_integer(i1) and is_integer(i2) and is_list(list) and is_integer(i3) do 213 | {:ok, table} 214 | end 215 | 216 | def continuation_table({table, _, _, i1, _match_spec, list, i2, i3}) 217 | when is_integer(i1) and is_list(list) and is_integer(i2) and is_integer(i3) do 218 | {:ok, table} 219 | end 220 | 221 | def continuation_table(_), do: {:error, :invalid_continuation} 222 | 223 | def valid_continuation?(continuation), do: match?({:ok, _}, continuation_table(continuation)) 224 | 225 | defmacro catch_invalid_continuation(continuation, do: do_block) do 226 | quote do 227 | try do 228 | case ETS.Utils.continuation_table(unquote(continuation)) do 229 | {:ok, :"$end_of_table"} -> 230 | unquote(do_block) 231 | 232 | {:error, :invalid_continuation} -> 233 | {:error, :invalid_continuation} 234 | 235 | {:ok, table} -> 236 | catch_read_protected table do 237 | catch_table_not_found table do 238 | unquote(do_block) 239 | end 240 | end 241 | end 242 | rescue 243 | e in ArgumentError -> 244 | if ETS.Utils.valid_continuation?(unquote(continuation)) do 245 | {:error, :invalid_continuation} 246 | else 247 | reraise(e, __STACKTRACE__) 248 | end 249 | end 250 | end 251 | end 252 | 253 | defmacro catch_write_protected(table, do: do_block) do 254 | quote do 255 | try do 256 | unquote(do_block) 257 | rescue 258 | e in ArgumentError -> 259 | info = :ets.info(unquote(table)) 260 | 261 | if info[:protection] == :public or info[:owner] == self() do 262 | reraise(e, __STACKTRACE__) 263 | else 264 | {:error, :write_protected} 265 | end 266 | end 267 | end 268 | end 269 | 270 | defmacro catch_read_protected(table, do: do_block) do 271 | quote do 272 | try do 273 | unquote(do_block) 274 | rescue 275 | e in ArgumentError -> 276 | info = :ets.info(unquote(table)) 277 | 278 | if info[:protection] in [:public, :protected] or info[:owner] == self() do 279 | reraise(e, __STACKTRACE__) 280 | else 281 | {:error, :read_protected} 282 | end 283 | end 284 | end 285 | end 286 | 287 | defmacro unwrap_or_raise(expr) do 288 | {func, arity} = __CALLER__.function 289 | mod = __CALLER__.module 290 | 291 | quote do 292 | case unquote(expr) do 293 | {:ok, value} -> 294 | value 295 | 296 | {:error, reason} -> 297 | raise "#{inspect(unquote(mod))}.#{unquote(func)}/#{unquote(arity)} returned {:error, #{inspect(reason)}}" 298 | end 299 | end 300 | end 301 | 302 | defmacro unwrap(expr) do 303 | quote do 304 | {:ok, value} = unquote(expr) 305 | value 306 | end 307 | end 308 | 309 | defmacro catch_recipient_already_owns_table(table, pid, do: do_block) do 310 | quote do 311 | try do 312 | unquote(do_block) 313 | rescue 314 | e in ArgumentError -> 315 | info = :ets.info(unquote(table)) 316 | 317 | case info[:owner] do 318 | ^unquote(pid) -> {:error, :recipient_already_owns_table} 319 | _ -> reraise(e, __STACKTRACE__) 320 | end 321 | end 322 | end 323 | end 324 | 325 | defmacro catch_recipient_not_alive(pid, do: do_block) do 326 | quote do 327 | try do 328 | unquote(do_block) 329 | rescue 330 | e in ArgumentError -> 331 | case process_alive_safe(unquote(pid)) do 332 | true -> reraise(e, __STACKTRACE__) 333 | false -> {:error, :recipient_not_alive} 334 | error -> error 335 | end 336 | end 337 | end 338 | end 339 | 340 | def process_alive_safe(pid) when is_pid(pid) do 341 | Process.alive?(pid) 342 | rescue 343 | ArgumentError -> {:error, :recipient_not_local} 344 | end 345 | 346 | def process_alive_safe(_), do: {:error, :recipient_not_pid} 347 | 348 | defmacro catch_sender_not_table_owner(table, do: do_block) do 349 | quote do 350 | try do 351 | unquote(do_block) 352 | rescue 353 | e in ArgumentError -> 354 | info = :ets.info(unquote(table)) 355 | self = self() 356 | 357 | case info[:owner] do 358 | ^self -> reraise(e, __STACKTRACE__) 359 | _ -> {:error, :sender_not_table_owner} 360 | end 361 | end 362 | end 363 | end 364 | end 365 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ETS.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ets, 7 | version: "0.9.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | docs: [ 14 | main: "ETS", 15 | extras: ["README.md"], 16 | source_ref: "master" 17 | ], 18 | package: package(), 19 | source_url: "https://github.com/TheFirstAvenger/ets", 20 | test_coverage: [tool: ExCoveralls], 21 | preferred_cli_env: [ 22 | coveralls: :test, 23 | "coveralls.detail": :test, 24 | "coveralls.post": :test, 25 | "coveralls.html": :test 26 | ], 27 | aliases: aliases(), 28 | dialyzer: [ 29 | ignore_warnings: ".dialyzer_ignore.exs", 30 | list_unused_filters: true, 31 | plt_file: {:no_warn, "plts/ets.plt"} 32 | ] 33 | ] 34 | end 35 | 36 | # Run "mix help compile.app" to learn about applications. 37 | def application do 38 | [ 39 | extra_applications: [:logger] 40 | ] 41 | end 42 | 43 | # Specifies which paths to compile per environment. 44 | defp elixirc_paths(:test), do: ["lib", "test/support"] 45 | defp elixirc_paths(_), do: ["lib"] 46 | 47 | # Run "mix help deps" to learn about dependencies. 48 | defp deps do 49 | [ 50 | {:ex_unit_notifier, "~> 1.2", only: :test}, 51 | {:mix_test_watch, "~> 1.1", only: :dev, runtime: false}, 52 | {:earmark, "~> 1.4", only: :dev, runtime: false}, 53 | {:ex_doc, "~> 0.28", only: :dev, runtime: false}, 54 | {:excoveralls, "~> 0.14.4", only: :test}, 55 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 56 | {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false} 57 | ] 58 | end 59 | 60 | defp description do 61 | "Elixir wrapper for the Erlang :ets module." 62 | end 63 | 64 | defp package do 65 | [ 66 | licenses: ["MIT"], 67 | links: %{"GitHub" => "https://github.com/TheFirstAvenger/ets"} 68 | ] 69 | end 70 | 71 | defp aliases do 72 | [ 73 | compile: ["compile --warnings-as-errors"], 74 | test: ["test --warnings-as-errors"] 75 | ] 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "credo": {:hex, :credo, "1.6.3", "0a9f8925dbc8f940031b789f4623fc9a0eea99d3eed600fe831e403eb96c6a83", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1167cde00e6661d740fc54da2ee268e35d3982f027399b64d3e2e83af57a1180"}, 5 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 6 | "earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, 8 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 9 | "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, 10 | "ex_unit_notifier": {:hex, :ex_unit_notifier, "1.2.0", "73ced2ecee0f2da0705e372c21ce61e4e5d927ddb797f73928e52818b9cc1754", [:mix], [], "hexpm", "f38044c9d50de68ad7f0aec4d781a10d9f1c92c62b36bf0227ec0aaa96aee332"}, 11 | "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, 12 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 13 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 14 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 15 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 16 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 20 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 21 | "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, 22 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"}, 23 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 26 | } 27 | -------------------------------------------------------------------------------- /plts/ignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheFirstAvenger/ets/c1689f289bd86e2867278f92e91a93699cdfb6f4/plts/ignore -------------------------------------------------------------------------------- /test/ets/bag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BagTest do 2 | use ExUnit.Case 3 | 4 | alias ETS.Bag 5 | alias ETS.TestUtils 6 | 7 | doctest ETS.Bag 8 | 9 | describe "Named Tables Start" do 10 | test "Duplicate Bag" do 11 | name = table_name() 12 | assert %Bag{} = Bag.new!(name: name) 13 | assert %{name: ^name, named_table: true, type: :duplicate_bag} = table_info(name) 14 | end 15 | 16 | test "Bag" do 17 | name = table_name() 18 | assert %Bag{} = Bag.new!(name: name, duplicate: false) 19 | assert %{name: ^name, named_table: true, type: :bag} = table_info(name) 20 | end 21 | end 22 | 23 | describe "Unnamed Tables Start" do 24 | test "Duplicate Bag" do 25 | assert %Bag{} = bag = Bag.new!() 26 | assert %{named_table: false, type: :duplicate_bag} = table_info(bag) 27 | end 28 | 29 | test "Bag" do 30 | assert %Bag{} = bag = Bag.new!(duplicate: false) 31 | assert %{named_table: false, type: :bag} = table_info(bag) 32 | end 33 | end 34 | 35 | describe "Options bag correctly" do 36 | test "Access" do 37 | assert %{protection: :private} = table_info(Bag.new!(protection: :private)) 38 | 39 | assert %{protection: :public} = table_info(Bag.new!(protection: :public)) 40 | 41 | assert %{protection: :protected} = table_info(Bag.new!(protection: :protected)) 42 | end 43 | 44 | test "Heir" do 45 | slf = self() 46 | assert %{heir: :none} = table_info(Bag.new!(heir: :none)) 47 | assert %{heir: ^slf} = table_info(Bag.new!(heir: {slf, :some_data})) 48 | end 49 | 50 | test "Keypos" do 51 | assert %{keypos: 5} = table_info(Bag.new!(keypos: 5)) 52 | assert %{keypos: 55} = table_info(Bag.new!(keypos: 55)) 53 | end 54 | 55 | test "Read Concurrency" do 56 | assert %{read_concurrency: true} = table_info(Bag.new!(read_concurrency: true)) 57 | assert %{read_concurrency: false} = table_info(Bag.new!(read_concurrency: false)) 58 | end 59 | 60 | test "Write Concurrency" do 61 | assert %{write_concurrency: true} = table_info(Bag.new!(write_concurrency: true)) 62 | assert %{write_concurrency: false} = table_info(Bag.new!(write_concurrency: false)) 63 | 64 | if ETS.TestUtils.otp25?() do 65 | assert %{write_concurrency: :auto} = table_info(Bag.new!(write_concurrency: :auto)) 66 | end 67 | end 68 | 69 | test "Compressed" do 70 | assert %{compressed: true} = table_info(Bag.new!(compressed: true)) 71 | assert %{compressed: false} = table_info(Bag.new!(compressed: false)) 72 | end 73 | end 74 | 75 | describe "Rejects bad options" do 76 | test "Duplicate" do 77 | assert_raise RuntimeError, 78 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:duplicate, :this_isnt_a_boolean}}}", 79 | fn -> 80 | Bag.new!(duplicate: :this_isnt_a_boolean) 81 | end 82 | end 83 | 84 | test "Access" do 85 | assert_raise RuntimeError, 86 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:protection, :nobody}}}", 87 | fn -> 88 | Bag.new!(protection: :nobody) 89 | end 90 | end 91 | 92 | test "Heir" do 93 | assert_raise RuntimeError, 94 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:heir, :nobody}}}", 95 | fn -> 96 | Bag.new!(heir: :nobody) 97 | end 98 | 99 | assert_raise RuntimeError, 100 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:heir, {:not_a_pid, :data}}}}", 101 | fn -> Bag.new!(heir: {:not_a_pid, :data}) end 102 | end 103 | 104 | test "Keypos" do 105 | assert_raise RuntimeError, 106 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:keypos, -1}}}", 107 | fn -> 108 | Bag.new!(keypos: -1) 109 | end 110 | 111 | assert_raise RuntimeError, 112 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:keypos, :not_a_number}}}", 113 | fn -> 114 | Bag.new!(keypos: :not_a_number) 115 | end 116 | end 117 | 118 | test "Read Concurrency" do 119 | assert_raise RuntimeError, 120 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:read_concurrency, :not_a_boolean}}}", 121 | fn -> Bag.new!(read_concurrency: :not_a_boolean) end 122 | end 123 | 124 | test "Write Concurrency" do 125 | assert_raise RuntimeError, 126 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:write_concurrency, :not_a_boolean}}}", 127 | fn -> Bag.new!(write_concurrency: :not_a_boolean) end 128 | end 129 | 130 | test "Compressed" do 131 | assert_raise RuntimeError, 132 | "ETS.Bag.new!/1 returned {:error, {:invalid_option, {:compressed, :not_a_boolean}}}", 133 | fn -> Bag.new!(compressed: :not_a_boolean) end 134 | end 135 | end 136 | 137 | describe "Add" do 138 | test "add!/2 raises on error" do 139 | bag = Bag.new!() 140 | 141 | assert_raise RuntimeError, "ETS.Bag.add!/2 returned {:error, :invalid_record}", fn -> 142 | Bag.add!(bag, [:a]) 143 | end 144 | 145 | Bag.delete!(bag) 146 | 147 | assert_raise RuntimeError, "ETS.Bag.add!/2 returned {:error, :table_not_found}", fn -> 148 | Bag.add!(bag, {:a}) 149 | end 150 | 151 | bag2 = Bag.new!(keypos: 3) 152 | 153 | assert_raise RuntimeError, "ETS.Bag.add!/2 returned {:error, :record_too_small}", fn -> 154 | Bag.add!(bag2, {:a, :b}) 155 | end 156 | 157 | assert_raise RuntimeError, "ETS.Bag.add!/2 returned {:error, :record_too_small}", fn -> 158 | Bag.add!(bag2, [{:a, :b}, {:c}]) 159 | end 160 | end 161 | 162 | test "add_new!/2 raises on error" do 163 | bag = Bag.new!() 164 | 165 | assert_raise RuntimeError, "ETS.Bag.add_new!/2 returned {:error, :invalid_record}", fn -> 166 | Bag.add_new!(bag, [:a]) 167 | end 168 | 169 | Bag.delete!(bag) 170 | 171 | assert_raise RuntimeError, 172 | "ETS.Bag.add_new!/2 returned {:error, :table_not_found}", 173 | fn -> 174 | Bag.add_new!(bag, {:a}) 175 | end 176 | 177 | bag2 = Bag.new!(keypos: 3) 178 | 179 | assert_raise RuntimeError, "ETS.Bag.add_new!/2 returned {:error, :record_too_small}", fn -> 180 | Bag.add_new!(bag2, {:a, :b}) 181 | end 182 | 183 | assert_raise RuntimeError, "ETS.Bag.add_new!/2 returned {:error, :record_too_small}", fn -> 184 | Bag.add_new!(bag2, [{:a, :b}, {:c}]) 185 | end 186 | end 187 | end 188 | 189 | describe "Lookup" do 190 | test "lookup_element!/3 raises on error" do 191 | bag = Bag.new!() 192 | 193 | assert_raise RuntimeError, 194 | "ETS.Bag.lookup_element!/3 returned {:error, :key_not_found}", 195 | fn -> 196 | Bag.lookup_element!(bag, :not_a_key, 2) 197 | end 198 | 199 | Bag.add!(bag, {:a, :b, :c, :d, :e}) 200 | Bag.add!(bag, {:a, :e, :f, :g}) 201 | 202 | assert_raise RuntimeError, 203 | "ETS.Bag.lookup_element!/3 returned {:error, :position_out_of_bounds}", 204 | fn -> Bag.lookup_element!(bag, :a, 5) end 205 | 206 | assert_raise RuntimeError, 207 | "ETS.Bag.lookup_element!/3 returned {:error, :position_out_of_bounds}", 208 | fn -> Bag.lookup_element!(bag, :a, 6) end 209 | 210 | Bag.delete!(bag) 211 | 212 | assert_raise RuntimeError, 213 | "ETS.Bag.lookup_element!/3 returned {:error, :table_not_found}", 214 | fn -> Bag.lookup_element!(bag, :not_a_key, 2) end 215 | end 216 | end 217 | 218 | describe "Match" do 219 | test "match!/2 raises on error" do 220 | bag = Bag.new!() 221 | Bag.delete(bag) 222 | 223 | assert_raise RuntimeError, "ETS.Bag.match!/2 returned {:error, :table_not_found}", fn -> 224 | Bag.match!(bag, {:a}) 225 | end 226 | end 227 | 228 | test "match!/3 raises on error" do 229 | bag = Bag.new!() 230 | Bag.delete(bag) 231 | 232 | assert_raise RuntimeError, "ETS.Bag.match!/3 returned {:error, :table_not_found}", fn -> 233 | Bag.match!(bag, {:a}, 1) 234 | end 235 | end 236 | 237 | test "match!/1 raises on error" do 238 | assert_raise RuntimeError, 239 | "ETS.Bag.match!/1 returned {:error, :invalid_continuation}", 240 | fn -> 241 | Bag.match!(:not_a_continuation) 242 | end 243 | end 244 | 245 | test "match_delete!/2 raises on error" do 246 | bag = Bag.new!() 247 | Bag.delete(bag) 248 | 249 | assert_raise RuntimeError, 250 | "ETS.Bag.match_delete!/2 returned {:error, :table_not_found}", 251 | fn -> 252 | Bag.match_delete!(bag, {:a}) 253 | end 254 | end 255 | 256 | test "match_object!/2 raises on error" do 257 | bag = Bag.new!() 258 | Bag.delete(bag) 259 | 260 | assert_raise RuntimeError, 261 | "ETS.Bag.match_object!/2 returned {:error, :table_not_found}", 262 | fn -> 263 | Bag.match_object!(bag, {:a}) 264 | end 265 | end 266 | 267 | test "match_object/3 reaches end of table" do 268 | bag = Bag.new!() 269 | Bag.add!(bag, {:w, :x, :y, :z}) 270 | assert {:ok, {[], :end_of_table}} = Bag.match_object(bag, {:_, :b, :_, :_}, 1) 271 | 272 | Bag.add!(bag, {:a, :b, :c, :d}) 273 | assert {:ok, {results, :end_of_table}} = Bag.match_object(bag, {:"$1", :b, :"$2", :_}, 2) 274 | assert results == [{:a, :b, :c, :d}] 275 | end 276 | 277 | test "match_object!/3 raises on error" do 278 | bag = Bag.new!() 279 | Bag.delete(bag) 280 | 281 | assert_raise RuntimeError, 282 | "ETS.Bag.match_object!/3 returned {:error, :table_not_found}", 283 | fn -> 284 | Bag.match_object!(bag, {:a}, 1) 285 | end 286 | end 287 | 288 | test "match_object/1 finds less matches than the limit" do 289 | bag = Bag.new!() 290 | Bag.add!(bag, [{:a, :b, :c, :d}, {:a, :b, :e, :f}, {:g, :b, :h, :i}]) 291 | {:ok, {_result, continuation}} = Bag.match_object(bag, {:_, :b, :_, :_}, 2) 292 | 293 | assert {:ok, {results, :end_of_table}} = Bag.match_object(continuation) 294 | assert results == [{:g, :b, :h, :i}] 295 | end 296 | 297 | test "match_object!/1 raises on error" do 298 | assert_raise RuntimeError, 299 | "ETS.Bag.match_object!/1 returned {:error, :invalid_continuation}", 300 | fn -> 301 | Bag.match_object!(:not_a_continuation) 302 | end 303 | end 304 | end 305 | 306 | describe "Select" do 307 | test "select!/2 raises on error" do 308 | bag = Bag.new!() 309 | Bag.delete(bag) 310 | 311 | assert_raise RuntimeError, "ETS.Bag.select!/2 returned {:error, :table_not_found}", fn -> 312 | Bag.select!(bag, []) 313 | end 314 | end 315 | 316 | test "select_delete!/2 raises on error" do 317 | bag = Bag.new!() 318 | Bag.delete(bag) 319 | 320 | assert_raise RuntimeError, 321 | "ETS.Bag.select_delete!/2 returned {:error, :table_not_found}", 322 | fn -> 323 | Bag.select_delete!(bag, []) 324 | end 325 | end 326 | end 327 | 328 | describe "Has Key" do 329 | test "has_key!/2 raises on error" do 330 | bag = Bag.new!() 331 | Bag.delete(bag) 332 | 333 | assert_raise RuntimeError, "ETS.Bag.has_key!/2 returned {:error, :table_not_found}", fn -> 334 | Bag.has_key!(bag, :key) 335 | end 336 | end 337 | end 338 | 339 | describe "To List" do 340 | test "to_list!/1 raises on error" do 341 | bag = Bag.new!() 342 | Bag.delete(bag) 343 | 344 | assert_raise RuntimeError, "ETS.Bag.to_list!/1 returned {:error, :table_not_found}", fn -> 345 | Bag.to_list!(bag) 346 | end 347 | end 348 | end 349 | 350 | describe "Delete" do 351 | test "delete!/2 raises on error" do 352 | bag = Bag.new!() 353 | Bag.delete!(bag) 354 | 355 | assert_raise RuntimeError, "ETS.Bag.delete!/1 returned {:error, :table_not_found}", fn -> 356 | Bag.delete!(bag) 357 | end 358 | end 359 | 360 | test "delete!/1 raises on error" do 361 | bag = Bag.new!() 362 | Bag.delete!(bag) 363 | 364 | assert_raise RuntimeError, "ETS.Bag.delete!/2 returned {:error, :table_not_found}", fn -> 365 | Bag.delete!(bag, :a) 366 | end 367 | end 368 | 369 | test "delete_all!/1 raises on error" do 370 | bag = Bag.new!() 371 | Bag.delete!(bag) 372 | 373 | assert_raise RuntimeError, 374 | "ETS.Bag.delete_all!/1 returned {:error, :table_not_found}", 375 | fn -> 376 | Bag.delete_all!(bag) 377 | end 378 | end 379 | end 380 | 381 | describe "Wrap Existing" do 382 | test "wrap_existing!/1 raises on error" do 383 | assert_raise RuntimeError, 384 | "ETS.Bag.wrap_existing!/1 returned {:error, :table_not_found}", 385 | fn -> 386 | Bag.wrap_existing!(:not_a_table) 387 | end 388 | end 389 | end 390 | 391 | describe "Get Table" do 392 | test "get_table!/1 returns table" do 393 | table = :ets.new(nil, [:bag]) 394 | bag = Bag.wrap_existing!(table) 395 | assert table == Bag.get_table!(bag) 396 | end 397 | end 398 | 399 | describe "Give Away give_away!/3" do 400 | test "success" do 401 | recipient_pid = self() 402 | 403 | spawn(fn -> 404 | bag = Bag.new!() 405 | Bag.give_away!(bag, recipient_pid) 406 | end) 407 | 408 | assert {:ok, %{bag: %Bag{}, gift: []}} = Bag.accept() 409 | end 410 | 411 | test "timeout" do 412 | assert {:error, :timeout} = Bag.accept(10) 413 | end 414 | 415 | test "cannot give to process which already owns table" do 416 | assert_raise RuntimeError, 417 | "ETS.Bag.give_away!/3 returned {:error, :recipient_already_owns_table}", 418 | fn -> 419 | bag = Bag.new!() 420 | Bag.give_away!(bag, self()) 421 | end 422 | end 423 | 424 | test "cannot give to process which is not alive" do 425 | assert_raise RuntimeError, 426 | "ETS.Bag.give_away!/3 returned {:error, :recipient_not_alive}", 427 | fn -> 428 | bag = Bag.new!() 429 | Bag.give_away!(bag, TestUtils.dead_pid()) 430 | end 431 | end 432 | 433 | test "cannot give a table belonging to another process" do 434 | sender_pid = self() 435 | 436 | _owner_pid = 437 | spawn_link(fn -> 438 | bag = Bag.new!() 439 | send(sender_pid, bag) 440 | Process.sleep(:infinity) 441 | end) 442 | 443 | assert_receive bag 444 | 445 | recipient_pid = spawn_link(fn -> Process.sleep(:infinity) end) 446 | 447 | assert_raise RuntimeError, 448 | "ETS.Bag.give_away!/3 returned {:error, :sender_not_table_owner}", 449 | fn -> 450 | Bag.give_away!(bag, recipient_pid) 451 | end 452 | end 453 | end 454 | 455 | describe "Macro" do 456 | test "accept/5 success" do 457 | {:ok, recipient_pid} = start_supervised(ETS.TestServer) 458 | 459 | %Bag{table: table} = bag = Bag.new!() 460 | 461 | Bag.give_away!(bag, recipient_pid, :bag_test) 462 | 463 | assert_receive {:thank_you, %Bag{table: ^table}} 464 | end 465 | end 466 | 467 | def table_name, do: String.to_atom("table#{:rand.uniform(9_999_999)}") 468 | 469 | def table_info(%Bag{table: table}), do: table_info(table) 470 | 471 | def table_info(id) do 472 | id 473 | |> :ets.info() 474 | |> Enum.into(%{}) 475 | end 476 | end 477 | -------------------------------------------------------------------------------- /test/ets/set/key_value_set_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeyValueSetTest do 2 | use ExUnit.Case 3 | 4 | alias ETS.KeyValueSet 5 | alias ETS.Set 6 | alias ETS.TestUtils 7 | 8 | doctest ETS.KeyValueSet 9 | 10 | describe "New" do 11 | test "Named Ordered KeyValueSet" do 12 | name = table_name() 13 | assert %KeyValueSet{} = KeyValueSet.new!(name: name, ordered: true) 14 | assert %{name: ^name, named_table: true, type: :ordered_set} = table_info(name) 15 | end 16 | 17 | test "Named KeyValueSet" do 18 | name = table_name() 19 | assert %KeyValueSet{} = KeyValueSet.new!(name: name) 20 | assert %{name: ^name, named_table: true, type: :set} = table_info(name) 21 | end 22 | 23 | test "Unnamed Ordered KeyValueSet" do 24 | assert %KeyValueSet{} = set = KeyValueSet.new!(ordered: true) 25 | assert %{named_table: false, type: :ordered_set} = table_info(set) 26 | end 27 | 28 | test "Unnamed KeyValueSet" do 29 | assert %KeyValueSet{} = set = KeyValueSet.new!() 30 | assert %{named_table: false, type: :set} = table_info(set) 31 | end 32 | 33 | test "rejects existing name" do 34 | name = table_name() 35 | assert %KeyValueSet{} = KeyValueSet.new!(name: name) 36 | 37 | assert_raise( 38 | RuntimeError, 39 | "ETS.KeyValueSet.new!/1 returned {:error, :table_already_exists}", 40 | fn -> 41 | KeyValueSet.new!(name: name) 42 | end 43 | ) 44 | end 45 | end 46 | 47 | describe "Options set correctly" do 48 | test "Access" do 49 | assert %{protection: :private} = table_info(KeyValueSet.new!(protection: :private)) 50 | 51 | assert %{protection: :public} = table_info(KeyValueSet.new!(protection: :public)) 52 | 53 | assert %{protection: :protected} = table_info(KeyValueSet.new!(protection: :protected)) 54 | end 55 | 56 | test "Heir" do 57 | slf = self() 58 | assert %{heir: :none} = table_info(KeyValueSet.new!(heir: :none)) 59 | assert %{heir: ^slf} = table_info(KeyValueSet.new!(heir: {slf, :some_data})) 60 | end 61 | 62 | test "Read Concurrency" do 63 | assert %{read_concurrency: true} = table_info(KeyValueSet.new!(read_concurrency: true)) 64 | assert %{read_concurrency: false} = table_info(KeyValueSet.new!(read_concurrency: false)) 65 | end 66 | 67 | test "Write Concurrency" do 68 | assert %{write_concurrency: true} = table_info(KeyValueSet.new!(write_concurrency: true)) 69 | assert %{write_concurrency: false} = table_info(KeyValueSet.new!(write_concurrency: false)) 70 | 71 | if ETS.TestUtils.otp25?() do 72 | assert %{write_concurrency: :auto} = 73 | table_info(KeyValueSet.new!(write_concurrency: :auto)) 74 | end 75 | end 76 | 77 | test "Compressed" do 78 | assert %{compressed: true} = table_info(KeyValueSet.new!(compressed: true)) 79 | assert %{compressed: false} = table_info(KeyValueSet.new!(compressed: false)) 80 | end 81 | end 82 | 83 | describe "Rejects bad options" do 84 | test "Ordered" do 85 | assert_raise RuntimeError, 86 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:ordered, :this_isnt_a_boolean}}}", 87 | fn -> 88 | KeyValueSet.new!(ordered: :this_isnt_a_boolean) 89 | end 90 | end 91 | 92 | test "Access" do 93 | assert_raise RuntimeError, 94 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:protection, :nobody}}}", 95 | fn -> 96 | KeyValueSet.new!(protection: :nobody) 97 | end 98 | end 99 | 100 | test "Heir" do 101 | assert_raise RuntimeError, 102 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:heir, :nobody}}}", 103 | fn -> 104 | KeyValueSet.new!(heir: :nobody) 105 | end 106 | 107 | assert_raise RuntimeError, 108 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:heir, {:not_a_pid, :data}}}}", 109 | fn -> KeyValueSet.new!(heir: {:not_a_pid, :data}) end 110 | end 111 | 112 | test "Keypos" do 113 | assert_raise RuntimeError, 114 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:keypos, -1}}}", 115 | fn -> 116 | KeyValueSet.new!(keypos: -1) 117 | end 118 | 119 | assert_raise RuntimeError, 120 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:keypos, 3}}}", 121 | fn -> 122 | KeyValueSet.new!(keypos: 3) 123 | end 124 | 125 | assert_raise RuntimeError, 126 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:keypos, :this_is_not_a_number}}}", 127 | fn -> 128 | KeyValueSet.new!(keypos: :this_is_not_a_number) 129 | end 130 | 131 | assert_raise RuntimeError, 132 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:keypos, 1}}}", 133 | fn -> 134 | KeyValueSet.new!(keypos: 1) 135 | end 136 | end 137 | 138 | test "Read Concurrency" do 139 | assert_raise RuntimeError, 140 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:read_concurrency, :not_a_boolean}}}", 141 | fn -> KeyValueSet.new!(read_concurrency: :not_a_boolean) end 142 | end 143 | 144 | test "Write Concurrency" do 145 | assert_raise RuntimeError, 146 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:write_concurrency, :not_a_boolean}}}", 147 | fn -> KeyValueSet.new!(write_concurrency: :not_a_boolean) end 148 | end 149 | 150 | test "Compressed" do 151 | assert_raise RuntimeError, 152 | "ETS.KeyValueSet.new!/1 returned {:error, {:invalid_option, {:compressed, :not_a_boolean}}}", 153 | fn -> KeyValueSet.new!(compressed: :not_a_boolean) end 154 | end 155 | end 156 | 157 | describe "Wrap existing" do 158 | test "Rejects not a set" do 159 | table = :ets.new(nil, [:bag]) 160 | 161 | assert_raise RuntimeError, 162 | "ETS.KeyValueSet.wrap_existing!/1 returned {:error, :invalid_type}", 163 | fn -> KeyValueSet.wrap_existing!(table) end 164 | end 165 | 166 | test "Rejects invalid keypos" do 167 | table = :ets.new(nil, [:set, keypos: 2]) 168 | 169 | assert_raise RuntimeError, 170 | "ETS.KeyValueSet.wrap_existing!/1 returned {:error, :invalid_keypos}", 171 | fn -> KeyValueSet.wrap_existing!(table) end 172 | end 173 | 174 | test "Succeeds on valid table" do 175 | table = :ets.new(nil, [:set]) 176 | kvset = KeyValueSet.wrap_existing!(table) 177 | assert KeyValueSet.info!(kvset)[:id] == table 178 | end 179 | end 180 | 181 | describe "Info" do 182 | test "returns correct information" do 183 | set = KeyValueSet.new!(read_concurrency: false, compressed: true) 184 | info = set |> KeyValueSet.info!() |> Enum.into(%{}) 185 | assert table_info(set) == info 186 | 187 | assert %{read_concurrency: false, compressed: true} = info 188 | end 189 | 190 | test "returns correct information (tuple version)" do 191 | set = KeyValueSet.new!(read_concurrency: false, compressed: true) 192 | {:ok, info} = KeyValueSet.info(set) 193 | info = info |> Enum.into(%{}) 194 | assert table_info(set) == info 195 | 196 | assert %{read_concurrency: false, compressed: true} = info 197 | end 198 | 199 | test "force update flag" do 200 | set = KeyValueSet.new!() 201 | memory = KeyValueSet.info!(set)[:memory] 202 | 203 | 1..10 204 | |> Enum.each(fn _ -> KeyValueSet.put(set, :rand.uniform(), :rand.uniform()) end) 205 | 206 | assert memory == KeyValueSet.info!(set)[:memory] 207 | assert memory != KeyValueSet.info!(set, true)[:memory] 208 | end 209 | 210 | test "handles missing table" do 211 | set = KeyValueSet.new!() 212 | KeyValueSet.delete!(set) 213 | 214 | assert_raise RuntimeError, 215 | "ETS.KeyValueSet.info!/2 returned {:error, :table_not_found}", 216 | fn -> 217 | KeyValueSet.info!(set, true) 218 | end 219 | end 220 | end 221 | 222 | describe "Get Table" do 223 | test "returns table" do 224 | table = :ets.new(nil, [:set]) 225 | set = KeyValueSet.wrap_existing!(table) 226 | assert table == KeyValueSet.get_table!(set) 227 | end 228 | end 229 | 230 | describe "Put" do 231 | test "adds single entry to table" do 232 | set = KeyValueSet.new!() 233 | assert [] == KeyValueSet.to_list!(set) 234 | KeyValueSet.put!(set, :a, :b) 235 | assert [{:a, :b}] == KeyValueSet.to_list!(set) 236 | end 237 | 238 | test "replaces existing entry" do 239 | set = KeyValueSet.new!() 240 | assert [] == KeyValueSet.to_list!(set) 241 | KeyValueSet.put!(set, :a, :b) 242 | assert [{:a, :b}] == KeyValueSet.to_list!(set) 243 | KeyValueSet.put!(set, :a, :c) 244 | assert [{:a, :c}] == KeyValueSet.to_list!(set) 245 | end 246 | 247 | test "raises on error" do 248 | set = KeyValueSet.new!() 249 | 250 | KeyValueSet.delete!(set) 251 | 252 | assert_raise RuntimeError, 253 | "ETS.KeyValueSet.put!/3 returned {:error, :table_not_found}", 254 | fn -> 255 | KeyValueSet.put!(set, :a, :b) 256 | end 257 | end 258 | end 259 | 260 | describe "Put New" do 261 | test "adds single entry to table" do 262 | set = KeyValueSet.new!() 263 | assert [] == KeyValueSet.to_list!(set) 264 | KeyValueSet.put_new!(set, :a, :b) 265 | assert [{:a, :b}] == KeyValueSet.to_list!(set) 266 | end 267 | 268 | test "doesn't replace existing entry" do 269 | set = KeyValueSet.new!() 270 | assert [] == KeyValueSet.to_list!(set) 271 | KeyValueSet.put_new!(set, :a, :b) 272 | assert [{:a, :b}] == KeyValueSet.to_list!(set) 273 | KeyValueSet.put_new!(set, :a, :c) 274 | assert [{:a, :b}] == KeyValueSet.to_list!(set) 275 | end 276 | 277 | test "raises on error" do 278 | set = KeyValueSet.new!() 279 | KeyValueSet.delete!(set) 280 | 281 | assert_raise RuntimeError, 282 | "ETS.KeyValueSet.put_new!/3 returned {:error, :table_not_found}", 283 | fn -> 284 | KeyValueSet.put_new!(set, :a, :b) 285 | end 286 | end 287 | end 288 | 289 | describe "Get" do 290 | test "returns correct value" do 291 | set = KeyValueSet.new!() 292 | KeyValueSet.put(set, :a, :b) 293 | assert :b = KeyValueSet.get!(set, :a) 294 | end 295 | 296 | test "returns correct value with default" do 297 | set = KeyValueSet.new!() 298 | KeyValueSet.put(set, :a, :b) 299 | assert :b = KeyValueSet.get!(set, :a, :asdf) 300 | end 301 | 302 | test "returns nil when value missing" do 303 | set = KeyValueSet.new!() 304 | assert nil == KeyValueSet.get!(set, :a) 305 | end 306 | 307 | test "returns default when value missing and default specified" do 308 | set = KeyValueSet.new!() 309 | assert :asdf == KeyValueSet.get!(set, :a, :asdf) 310 | end 311 | 312 | test "raises on error" do 313 | set = KeyValueSet.new!() 314 | KeyValueSet.delete!(set) 315 | 316 | assert_raise RuntimeError, 317 | "ETS.KeyValueSet.get!/3 returned {:error, :table_not_found}", 318 | fn -> 319 | KeyValueSet.get!(set, :a) 320 | end 321 | end 322 | end 323 | 324 | describe "Has Key" do 325 | test "has_key!/2 raises on error" do 326 | set = KeyValueSet.new!() 327 | KeyValueSet.delete(set) 328 | 329 | assert_raise RuntimeError, 330 | "ETS.KeyValueSet.has_key!/2 returned {:error, :table_not_found}", 331 | fn -> 332 | KeyValueSet.has_key!(set, :key) 333 | end 334 | end 335 | end 336 | 337 | describe "First" do 338 | test "first!/1 requires ordered set" do 339 | set = KeyValueSet.new!() 340 | 341 | assert_raise RuntimeError, 342 | "ETS.KeyValueSet.first!/1 returned {:error, :set_not_ordered}", 343 | fn -> 344 | KeyValueSet.first!(set) 345 | end 346 | end 347 | end 348 | 349 | describe "Last" do 350 | test "last!/1 requires ordered set" do 351 | set = KeyValueSet.new!() 352 | 353 | assert_raise RuntimeError, 354 | "ETS.KeyValueSet.last!/1 returned {:error, :set_not_ordered}", 355 | fn -> 356 | KeyValueSet.last!(set) 357 | end 358 | end 359 | end 360 | 361 | describe "Next" do 362 | test "next!/2 requires ordered set" do 363 | set = KeyValueSet.new!() 364 | 365 | assert_raise RuntimeError, 366 | "ETS.KeyValueSet.next!/2 returned {:error, :set_not_ordered}", 367 | fn -> 368 | KeyValueSet.next!(set, :a) 369 | end 370 | end 371 | end 372 | 373 | describe "Previous" do 374 | test "previous!/2 requires ordered set" do 375 | set = KeyValueSet.new!() 376 | 377 | assert_raise RuntimeError, 378 | "ETS.KeyValueSet.previous!/2 returned {:error, :set_not_ordered}", 379 | fn -> 380 | KeyValueSet.previous!(set, :a) 381 | end 382 | end 383 | end 384 | 385 | describe "To List" do 386 | test "to_list!/1 raises on error" do 387 | set = KeyValueSet.new!() 388 | KeyValueSet.delete(set) 389 | 390 | assert_raise RuntimeError, 391 | "ETS.KeyValueSet.to_list!/1 returned {:error, :table_not_found}", 392 | fn -> 393 | KeyValueSet.to_list!(set) 394 | end 395 | end 396 | end 397 | 398 | describe "Delete" do 399 | test "delete!/2 raises on error" do 400 | set = KeyValueSet.new!() 401 | KeyValueSet.delete!(set) 402 | 403 | assert_raise RuntimeError, 404 | "ETS.KeyValueSet.delete!/1 returned {:error, :table_not_found}", 405 | fn -> 406 | KeyValueSet.delete!(set) 407 | end 408 | end 409 | 410 | test "delete!/1 raises on error" do 411 | set = KeyValueSet.new!() 412 | KeyValueSet.delete!(set) 413 | 414 | assert_raise RuntimeError, 415 | "ETS.KeyValueSet.delete!/2 returned {:error, :table_not_found}", 416 | fn -> 417 | KeyValueSet.delete!(set, :a) 418 | end 419 | end 420 | 421 | test "delete_all!/1 raises on error" do 422 | set = KeyValueSet.new!() 423 | KeyValueSet.delete!(set) 424 | 425 | assert_raise RuntimeError, 426 | "ETS.KeyValueSet.delete_all!/1 returned {:error, :table_not_found}", 427 | fn -> 428 | KeyValueSet.delete_all!(set) 429 | end 430 | end 431 | end 432 | 433 | describe "Give Away give_away/3" do 434 | test "success" do 435 | recipient_pid = self() 436 | 437 | spawn(fn -> 438 | bag = KeyValueSet.new!() 439 | KeyValueSet.give_away!(bag, recipient_pid) 440 | end) 441 | 442 | assert {:ok, %{kv_set: %KeyValueSet{}, gift: []}} = KeyValueSet.accept() 443 | end 444 | 445 | test "timeout" do 446 | assert {:error, :timeout} = KeyValueSet.accept(10) 447 | end 448 | 449 | test "cannot give to process which already owns table" do 450 | assert_raise RuntimeError, 451 | "ETS.KeyValueSet.give_away!/3 returned {:error, :recipient_already_owns_table}", 452 | fn -> 453 | kv_set = KeyValueSet.new!() 454 | KeyValueSet.give_away!(kv_set, self()) 455 | end 456 | end 457 | 458 | test "cannot give to process which is not alive" do 459 | assert_raise RuntimeError, 460 | "ETS.KeyValueSet.give_away!/3 returned {:error, :recipient_not_alive}", 461 | fn -> 462 | kv_set = KeyValueSet.new!() 463 | KeyValueSet.give_away!(kv_set, TestUtils.dead_pid()) 464 | end 465 | end 466 | 467 | test "cannot give a table belonging to another process" do 468 | sender_pid = self() 469 | 470 | _owner_pid = 471 | spawn_link(fn -> 472 | kv_set = KeyValueSet.new!() 473 | send(sender_pid, kv_set) 474 | Process.sleep(:infinity) 475 | end) 476 | 477 | assert_receive kv_set 478 | 479 | recipient_pid = spawn_link(fn -> Process.sleep(:infinity) end) 480 | 481 | assert_raise RuntimeError, 482 | "ETS.KeyValueSet.give_away!/3 returned {:error, :sender_not_table_owner}", 483 | fn -> 484 | KeyValueSet.give_away!(kv_set, recipient_pid) 485 | end 486 | end 487 | end 488 | 489 | describe "Macro" do 490 | test "accept/5 success" do 491 | {:ok, recipient_pid} = start_supervised(ETS.TestServer) 492 | 493 | %KeyValueSet{set: %Set{table: table}} = kv_set = KeyValueSet.new!() 494 | 495 | KeyValueSet.give_away!(kv_set, recipient_pid, :kv_test) 496 | 497 | assert_receive {:thank_you, %KeyValueSet{set: %Set{table: ^table}}} 498 | end 499 | end 500 | 501 | def table_name, do: String.to_atom("table#{:rand.uniform(9_999_999)}") 502 | 503 | def table_info(%KeyValueSet{set: %Set{table: table}}), do: table_info(table) 504 | 505 | def table_info(id) do 506 | id 507 | |> :ets.info() 508 | |> Enum.into(%{}) 509 | end 510 | end 511 | -------------------------------------------------------------------------------- /test/ets/set_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SetTest do 2 | use ExUnit.Case 3 | 4 | alias ETS.Set 5 | alias ETS.TestUtils 6 | 7 | doctest ETS.Set 8 | 9 | describe "New" do 10 | test "Named Ordered Set" do 11 | name = table_name() 12 | assert %Set{} = Set.new!(name: name, ordered: true) 13 | assert %{name: ^name, named_table: true, type: :ordered_set} = table_info(name) 14 | end 15 | 16 | test "Named Set" do 17 | name = table_name() 18 | assert %Set{} = Set.new!(name: name) 19 | assert %{name: ^name, named_table: true, type: :set} = table_info(name) 20 | end 21 | 22 | test "Unnamed Ordered Set" do 23 | assert %Set{} = set = Set.new!(ordered: true) 24 | assert %{named_table: false, type: :ordered_set} = table_info(set) 25 | end 26 | 27 | test "Unnamed Set" do 28 | assert %Set{} = set = Set.new!() 29 | assert %{named_table: false, type: :set} = table_info(set) 30 | end 31 | 32 | test "rejects existing name" do 33 | name = table_name() 34 | assert %Set{} = Set.new!(name: name) 35 | 36 | assert_raise(RuntimeError, "ETS.Set.new!/1 returned {:error, :table_already_exists}", fn -> 37 | Set.new!(name: name) 38 | end) 39 | end 40 | end 41 | 42 | describe "Options set correctly" do 43 | test "Access" do 44 | assert %{protection: :private} = table_info(Set.new!(protection: :private)) 45 | 46 | assert %{protection: :public} = table_info(Set.new!(protection: :public)) 47 | 48 | assert %{protection: :protected} = table_info(Set.new!(protection: :protected)) 49 | end 50 | 51 | test "Heir" do 52 | slf = self() 53 | assert %{heir: :none} = table_info(Set.new!(heir: :none)) 54 | assert %{heir: ^slf} = table_info(Set.new!(heir: {slf, :some_data})) 55 | end 56 | 57 | test "Keypos" do 58 | assert %{keypos: 5} = table_info(Set.new!(keypos: 5)) 59 | assert %{keypos: 55} = table_info(Set.new!(keypos: 55)) 60 | end 61 | 62 | test "Read Concurrency" do 63 | assert %{read_concurrency: true} = table_info(Set.new!(read_concurrency: true)) 64 | assert %{read_concurrency: false} = table_info(Set.new!(read_concurrency: false)) 65 | end 66 | 67 | test "Write Concurrency" do 68 | assert %{write_concurrency: true} = table_info(Set.new!(write_concurrency: true)) 69 | assert %{write_concurrency: false} = table_info(Set.new!(write_concurrency: false)) 70 | 71 | if ETS.TestUtils.otp25?() do 72 | assert %{write_concurrency: :auto} = table_info(Set.new!(write_concurrency: :auto)) 73 | end 74 | end 75 | 76 | test "Compressed" do 77 | assert %{compressed: true} = table_info(Set.new!(compressed: true)) 78 | assert %{compressed: false} = table_info(Set.new!(compressed: false)) 79 | end 80 | end 81 | 82 | describe "Rejects bad options" do 83 | test "Ordered" do 84 | assert_raise RuntimeError, 85 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:ordered, :this_isnt_a_boolean}}}", 86 | fn -> 87 | Set.new!(ordered: :this_isnt_a_boolean) 88 | end 89 | end 90 | 91 | test "Access" do 92 | assert_raise RuntimeError, 93 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:protection, :nobody}}}", 94 | fn -> 95 | Set.new!(protection: :nobody) 96 | end 97 | end 98 | 99 | test "Heir" do 100 | assert_raise RuntimeError, 101 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:heir, :nobody}}}", 102 | fn -> 103 | Set.new!(heir: :nobody) 104 | end 105 | 106 | assert_raise RuntimeError, 107 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:heir, {:not_a_pid, :data}}}}", 108 | fn -> Set.new!(heir: {:not_a_pid, :data}) end 109 | end 110 | 111 | test "Keypos" do 112 | assert_raise RuntimeError, 113 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:keypos, -1}}}", 114 | fn -> 115 | Set.new!(keypos: -1) 116 | end 117 | 118 | assert_raise RuntimeError, 119 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:keypos, :not_a_number}}}", 120 | fn -> 121 | Set.new!(keypos: :not_a_number) 122 | end 123 | end 124 | 125 | test "Read Concurrency" do 126 | assert_raise RuntimeError, 127 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:read_concurrency, :not_a_boolean}}}", 128 | fn -> Set.new!(read_concurrency: :not_a_boolean) end 129 | end 130 | 131 | test "Write Concurrency" do 132 | assert_raise RuntimeError, 133 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:write_concurrency, :not_a_boolean}}}", 134 | fn -> Set.new!(write_concurrency: :not_a_boolean) end 135 | end 136 | 137 | test "Compressed" do 138 | assert_raise RuntimeError, 139 | "ETS.Set.new!/1 returned {:error, {:invalid_option, {:compressed, :not_a_boolean}}}", 140 | fn -> Set.new!(compressed: :not_a_boolean) end 141 | end 142 | end 143 | 144 | describe "Info" do 145 | test "returns correct information" do 146 | set = Set.new!(keypos: 4, read_concurrency: false, compressed: true) 147 | info = set |> Set.info!() |> Enum.into(%{}) 148 | assert table_info(set) == info 149 | 150 | assert %{keypos: 4, read_concurrency: false, compressed: true} = info 151 | end 152 | 153 | test "force update flag" do 154 | set = Set.new!() 155 | memory = Set.info!(set)[:memory] 156 | 157 | 1..10 158 | |> Enum.each(fn _ -> Set.put(set, {:rand.uniform(), :rand.uniform()}) end) 159 | 160 | assert memory == Set.info!(set)[:memory] 161 | assert memory != Set.info!(set, true)[:memory] 162 | end 163 | 164 | test "handles missing table" do 165 | set = Set.new!() 166 | Set.delete!(set) 167 | 168 | assert_raise RuntimeError, "ETS.Set.info!/2 returned {:error, :table_not_found}", fn -> 169 | Set.info!(set, true) 170 | end 171 | end 172 | end 173 | 174 | describe "Get Table" do 175 | test "returns table" do 176 | table = :ets.new(nil, [:set]) 177 | set = Set.wrap_existing!(table) 178 | assert table == Set.get_table!(set) 179 | end 180 | end 181 | 182 | describe "Put" do 183 | test "adds single entry to table" do 184 | set = Set.new!() 185 | assert [] == Set.to_list!(set) 186 | Set.put!(set, {:a, :b}) 187 | assert [{:a, :b}] == Set.to_list!(set) 188 | end 189 | 190 | test "adds multiple entries to table" do 191 | set = Set.new!(ordered: true) 192 | assert [] == Set.to_list!(set) 193 | Set.put!(set, [{:a, :b}, {:c, :d}, {:e, :f}]) 194 | assert [{:a, :b}, {:c, :d}, {:e, :f}] == Set.to_list!(set) 195 | end 196 | 197 | test "replaces existing entry" do 198 | set = Set.new!() 199 | assert [] == Set.to_list!(set) 200 | Set.put!(set, {:a, :b}) 201 | assert [{:a, :b}] == Set.to_list!(set) 202 | Set.put!(set, {:a, :c}) 203 | assert [{:a, :c}] == Set.to_list!(set) 204 | end 205 | 206 | test "raises on error" do 207 | set = Set.new!() 208 | 209 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :invalid_record}", fn -> 210 | Set.put!(set, [:a]) 211 | end 212 | 213 | Set.delete!(set) 214 | 215 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :table_not_found}", fn -> 216 | Set.put!(set, {:a}) 217 | end 218 | 219 | set2 = Set.new!(keypos: 3) 220 | 221 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :record_too_small}", fn -> 222 | Set.put!(set2, {:a, :b}) 223 | end 224 | 225 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :record_too_small}", fn -> 226 | Set.put!(set2, [{:a, :b}, {:c}]) 227 | end 228 | 229 | slf = self() 230 | 231 | spawn_link(fn -> 232 | set1 = Set.new!(protection: :protected) 233 | set2 = Set.new!(protection: :private) 234 | send(slf, {:table, set1, set2}) 235 | :timer.sleep(:infinity) 236 | end) 237 | 238 | {set1, set2} = 239 | receive do 240 | {:table, set1, set2} -> {set1, set2} 241 | end 242 | 243 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :write_protected}", fn -> 244 | Set.put!(set1, {:a, :b, :c}) 245 | end 246 | 247 | assert_raise RuntimeError, "ETS.Set.put!/2 returned {:error, :write_protected}", fn -> 248 | Set.put!(set2, {:a, :b, :c}) 249 | end 250 | end 251 | end 252 | 253 | describe "Put New" do 254 | test "adds single entry to table" do 255 | set = Set.new!() 256 | assert [] == Set.to_list!(set) 257 | Set.put_new!(set, {:a, :b}) 258 | assert [{:a, :b}] == Set.to_list!(set) 259 | end 260 | 261 | test "adds multiple entries to table" do 262 | set = Set.new!(ordered: true) 263 | assert [] == Set.to_list!(set) 264 | Set.put_new!(set, [{:a, :b}, {:c, :d}, {:e, :f}]) 265 | assert [{:a, :b}, {:c, :d}, {:e, :f}] == Set.to_list!(set) 266 | end 267 | 268 | test "doesn't replace existing entry" do 269 | set = Set.new!() 270 | assert [] == Set.to_list!(set) 271 | Set.put_new!(set, {:a, :b}) 272 | assert [{:a, :b}] == Set.to_list!(set) 273 | Set.put_new!(set, {:a, :c}) 274 | assert [{:a, :b}] == Set.to_list!(set) 275 | end 276 | 277 | test "raises on error" do 278 | set = Set.new!() 279 | 280 | assert_raise RuntimeError, "ETS.Set.put_new!/2 returned {:error, :invalid_record}", fn -> 281 | Set.put_new!(set, [:a]) 282 | end 283 | 284 | Set.delete!(set) 285 | 286 | assert_raise RuntimeError, 287 | "ETS.Set.put_new!/2 returned {:error, :table_not_found}", 288 | fn -> 289 | Set.put_new!(set, {:a}) 290 | end 291 | 292 | set2 = Set.new!(keypos: 3) 293 | 294 | assert_raise RuntimeError, "ETS.Set.put_new!/2 returned {:error, :record_too_small}", fn -> 295 | Set.put_new!(set2, {:a, :b}) 296 | end 297 | 298 | assert_raise RuntimeError, "ETS.Set.put_new!/2 returned {:error, :record_too_small}", fn -> 299 | Set.put_new!(set2, [{:a, :b}, {:c}]) 300 | end 301 | 302 | slf = self() 303 | 304 | spawn_link(fn -> 305 | set1 = Set.new!(protection: :protected) 306 | set2 = Set.new!(protection: :private) 307 | send(slf, {:table, set1, set2}) 308 | :timer.sleep(:infinity) 309 | end) 310 | 311 | {set1, set2} = 312 | receive do 313 | {:table, set1, set2} -> {set1, set2} 314 | end 315 | 316 | assert_raise RuntimeError, "ETS.Set.put_new!/2 returned {:error, :write_protected}", fn -> 317 | Set.put_new!(set1, {:a, :b, :c}) 318 | end 319 | 320 | assert_raise RuntimeError, "ETS.Set.put_new!/2 returned {:error, :write_protected}", fn -> 321 | Set.put_new!(set2, {:a, :b, :c}) 322 | end 323 | end 324 | end 325 | 326 | describe "Fetch" do 327 | test "returns correct value" do 328 | set = Set.new!() 329 | Set.put(set, {:a, :b}) 330 | assert {:ok, {:a, :b}} = Set.fetch(set, :a) 331 | end 332 | 333 | test "returns error when value missing" do 334 | set = Set.new!() 335 | assert {:error, :key_not_found} == Set.fetch(set, :a) 336 | end 337 | end 338 | 339 | describe "Get" do 340 | test "returns correct value" do 341 | set = Set.new!() 342 | Set.put(set, {:a, :b}) 343 | assert {:a, :b} = Set.get!(set, :a) 344 | end 345 | 346 | test "returns correct value with default" do 347 | set = Set.new!() 348 | Set.put(set, {:a, :b}) 349 | assert {:a, :b} = Set.get!(set, :a, :asdf) 350 | end 351 | 352 | test "returns nil when value missing" do 353 | set = Set.new!() 354 | assert nil == Set.get!(set, :a) 355 | end 356 | 357 | test "returns default when value missing and default specified" do 358 | set = Set.new!() 359 | assert :asdf == Set.get!(set, :a, :asdf) 360 | end 361 | 362 | test "raises on error" do 363 | set = Set.new!() 364 | Set.delete!(set) 365 | 366 | assert_raise RuntimeError, "ETS.Set.get!/3 returned {:error, :table_not_found}", fn -> 367 | Set.get!(set, :a) 368 | end 369 | 370 | slf = self() 371 | 372 | spawn_link(fn -> 373 | set = Set.new!(protection: :private) 374 | send(slf, {:table, set}) 375 | :timer.sleep(:infinity) 376 | end) 377 | 378 | set = 379 | receive do 380 | {:table, set} -> set 381 | end 382 | 383 | assert_raise RuntimeError, "ETS.Set.get!/3 returned {:error, :read_protected}", fn -> 384 | Set.get!(set, :a) 385 | end 386 | end 387 | end 388 | 389 | describe "get_element" do 390 | test "returns correct elements" do 391 | set = Set.new!() 392 | Set.put!(set, {:a, :b, :c, :d}) 393 | Set.put!(set, {:e, :f, :g, :h}) 394 | assert :a = Set.get_element!(set, :a, 1) 395 | assert :b = Set.get_element!(set, :a, 2) 396 | assert :c = Set.get_element!(set, :a, 3) 397 | assert :d = Set.get_element!(set, :a, 4) 398 | assert :e = Set.get_element!(set, :e, 1) 399 | assert :f = Set.get_element!(set, :e, 2) 400 | assert :g = Set.get_element!(set, :e, 3) 401 | assert :h = Set.get_element!(set, :e, 4) 402 | end 403 | 404 | test "raises on error" do 405 | set = Set.new!() 406 | 407 | assert_raise RuntimeError, "ETS.Set.get_element!/3 returned {:error, :key_not_found}", fn -> 408 | Set.get_element!(set, :not_a_key, 2) 409 | end 410 | 411 | Set.put!(set, {:a, :b, :c, :d}) 412 | 413 | assert_raise RuntimeError, 414 | "ETS.Set.get_element!/3 returned {:error, :position_out_of_bounds}", 415 | fn -> Set.get_element!(set, :a, 5) end 416 | 417 | Set.delete!(set) 418 | 419 | assert_raise RuntimeError, 420 | "ETS.Set.get_element!/3 returned {:error, :table_not_found}", 421 | fn -> Set.get_element!(set, :not_a_key, 2) end 422 | end 423 | end 424 | 425 | describe "Match" do 426 | test "match!/2 raises on error" do 427 | set = Set.new!() 428 | Set.delete(set) 429 | 430 | assert_raise RuntimeError, "ETS.Set.match!/2 returned {:error, :table_not_found}", fn -> 431 | Set.match!(set, {:a}) 432 | end 433 | end 434 | 435 | test "match!/3 raises on error" do 436 | set = Set.new!() 437 | Set.delete(set) 438 | 439 | assert_raise RuntimeError, "ETS.Set.match!/3 returned {:error, :table_not_found}", fn -> 440 | Set.match!(set, {:a}, 1) 441 | end 442 | end 443 | 444 | test "match!/1 raises on error" do 445 | assert_raise RuntimeError, 446 | "ETS.Set.match!/1 returned {:error, :invalid_continuation}", 447 | fn -> 448 | Set.match!(:not_a_continuation) 449 | end 450 | end 451 | 452 | test "match_delete!/2 raises on error" do 453 | set = Set.new!() 454 | Set.delete(set) 455 | 456 | assert_raise RuntimeError, 457 | "ETS.Set.match_delete!/2 returned {:error, :table_not_found}", 458 | fn -> 459 | Set.match_delete!(set, {:a}) 460 | end 461 | end 462 | 463 | test "match_object!/2 raises on error" do 464 | set = Set.new!() 465 | Set.delete(set) 466 | 467 | assert_raise RuntimeError, 468 | "ETS.Set.match_object!/2 returned {:error, :table_not_found}", 469 | fn -> 470 | Set.match_object!(set, {:a}) 471 | end 472 | end 473 | 474 | test "match_object/3 reaches end of table" do 475 | set = Set.new!() 476 | Set.put!(set, {:w, :x, :y, :z}) 477 | assert {:ok, {[], :end_of_table}} = Set.match_object(set, {:_, :b, :_, :_}, 1) 478 | 479 | Set.put!(set, {:a, :b, :c, :d}) 480 | assert {:ok, {results, :end_of_table}} = Set.match_object(set, {:"$1", :b, :"$2", :_}, 2) 481 | assert results == [{:a, :b, :c, :d}] 482 | end 483 | 484 | test "match_object!/3 raises on error" do 485 | set = Set.new!() 486 | Set.delete(set) 487 | 488 | assert_raise RuntimeError, 489 | "ETS.Set.match_object!/3 returned {:error, :table_not_found}", 490 | fn -> 491 | Set.match_object!(set, {:a}, 1) 492 | end 493 | end 494 | 495 | test "match_object/1 finds less matches than the limit" do 496 | set = Set.new!() 497 | Set.put!(set, [{:a, :b, :c, :d}, {:e, :b, :f, :g}, {:h, :b, :i, :j}]) 498 | {:ok, {_result, continuation}} = Set.match_object(set, {:_, :b, :_, :_}, 2) 499 | 500 | assert {:ok, {results, :end_of_table}} = Set.match_object(continuation) 501 | assert results == [{:h, :b, :i, :j}] 502 | end 503 | 504 | test "match_object!/1 raises on error" do 505 | assert_raise RuntimeError, 506 | "ETS.Set.match_object!/1 returned {:error, :invalid_continuation}", 507 | fn -> 508 | Set.match_object!(:not_a_continuation) 509 | end 510 | end 511 | end 512 | 513 | describe "Select" do 514 | test "select!/2 raises on error" do 515 | set = Set.new!() 516 | Set.delete(set) 517 | 518 | assert_raise RuntimeError, "ETS.Set.select!/2 returned {:error, :table_not_found}", fn -> 519 | Set.select!(set, []) 520 | end 521 | end 522 | 523 | test "select_delete!/2 raises on error" do 524 | set = Set.new!() 525 | Set.delete(set) 526 | 527 | assert_raise RuntimeError, 528 | "ETS.Set.select_delete!/2 returned {:error, :table_not_found}", 529 | fn -> 530 | Set.select_delete!(set, []) 531 | end 532 | end 533 | 534 | test "select!/3 raises on error" do 535 | set = Set.new!() 536 | Set.delete(set) 537 | 538 | assert_raise RuntimeError, "ETS.Set.select!/3 returned {:error, :table_not_found}", fn -> 539 | Set.select!(set, [{[:"$"], [], [:"$_"]}], 10) 540 | end 541 | end 542 | 543 | test "select!/1 raises on error" do 544 | set = Set.new!() 545 | Set.put!(set, {1, "one"}) 546 | Set.put!(set, {2, "two"}) 547 | 548 | {_, continuation} = Set.select!(set, [{:_, [], [:"$_"]}], 1) 549 | 550 | Set.delete(set) 551 | 552 | assert_raise RuntimeError, "ETS.Set.select!/1 returned {:error, :table_not_found}", fn -> 553 | Set.select!(continuation) 554 | end 555 | end 556 | 557 | test "select!/3 continuation can be used with select!/1" do 558 | set = Set.new!() 559 | Set.put!(set, {1, "one"}) 560 | Set.put!(set, {2, "two"}) 561 | 562 | {[{2, "two"}], continuation} = Set.select!(set, [{:_, [], [:"$_"]}], 1) 563 | 564 | assert {[{1, "one"}], continuation} = Set.select!(continuation) 565 | 566 | assert :"$end_of_table" = Set.select!(continuation) 567 | end 568 | end 569 | 570 | describe "Has Key" do 571 | test "has_key!/2 raises on error" do 572 | set = Set.new!() 573 | Set.delete(set) 574 | 575 | assert_raise RuntimeError, "ETS.Set.has_key!/2 returned {:error, :table_not_found}", fn -> 576 | Set.has_key!(set, :key) 577 | end 578 | end 579 | end 580 | 581 | describe "First" do 582 | test "first!/1 requires ordered set" do 583 | set = Set.new!() 584 | 585 | assert_raise RuntimeError, "ETS.Set.first!/1 returned {:error, :set_not_ordered}", fn -> 586 | Set.first!(set) 587 | end 588 | end 589 | end 590 | 591 | describe "Last" do 592 | test "last!/1 requires ordered set" do 593 | set = Set.new!() 594 | 595 | assert_raise RuntimeError, "ETS.Set.last!/1 returned {:error, :set_not_ordered}", fn -> 596 | Set.last!(set) 597 | end 598 | end 599 | end 600 | 601 | describe "Next" do 602 | test "next!/2 requires ordered set" do 603 | set = Set.new!() 604 | 605 | assert_raise RuntimeError, "ETS.Set.next!/2 returned {:error, :set_not_ordered}", fn -> 606 | Set.next!(set, :a) 607 | end 608 | end 609 | end 610 | 611 | describe "Previous" do 612 | test "previous!/2 requires ordered set" do 613 | set = Set.new!() 614 | 615 | assert_raise RuntimeError, "ETS.Set.previous!/2 returned {:error, :set_not_ordered}", fn -> 616 | Set.previous!(set, :a) 617 | end 618 | end 619 | end 620 | 621 | describe "To List" do 622 | test "to_list!/1 raises on error" do 623 | set = Set.new!() 624 | Set.delete(set) 625 | 626 | assert_raise RuntimeError, "ETS.Set.to_list!/1 returned {:error, :table_not_found}", fn -> 627 | Set.to_list!(set) 628 | end 629 | end 630 | end 631 | 632 | describe "Delete" do 633 | test "delete!/1 raises on error" do 634 | set = Set.new!() 635 | Set.delete!(set) 636 | 637 | assert_raise RuntimeError, "ETS.Set.delete!/1 returned {:error, :table_not_found}", fn -> 638 | Set.delete!(set) 639 | end 640 | end 641 | 642 | test "delete!/2 raises on error" do 643 | set = Set.new!() 644 | Set.delete!(set) 645 | 646 | assert_raise RuntimeError, "ETS.Set.delete!/2 returned {:error, :table_not_found}", fn -> 647 | Set.delete!(set, :a) 648 | end 649 | end 650 | 651 | test "delete_all!/1 raises on error" do 652 | set = Set.new!() 653 | Set.delete!(set) 654 | 655 | assert_raise RuntimeError, 656 | "ETS.Set.delete_all!/1 returned {:error, :table_not_found}", 657 | fn -> 658 | Set.delete_all!(set) 659 | end 660 | end 661 | end 662 | 663 | describe "Update" do 664 | test "update_element!/3 fails if table no longer exists" do 665 | set = Set.new!() 666 | Set.delete!(set) 667 | 668 | assert_raise RuntimeError, 669 | "ETS.Set.update_element!/3 returned {:error, :table_not_found}", 670 | fn -> 671 | Set.update_element!(set, :a, {2, :b}) 672 | end 673 | end 674 | 675 | test "update_element!/3 must have write access to the table" do 676 | test_pid = self() 677 | 678 | spawn_link(fn -> 679 | set = Set.new!() 680 | Set.put!(set, {:a, :b, :c}) 681 | send(test_pid, set) 682 | Process.sleep(:infinity) 683 | end) 684 | 685 | assert_receive set 686 | 687 | assert_raise RuntimeError, 688 | "ETS.Set.update_element!/3 returned {:error, :write_protected}", 689 | fn -> 690 | Set.update_element!(set, :a, {2, :x}) 691 | end 692 | end 693 | 694 | test "update_element!/3 fails if position is out of bounds" do 695 | set = Set.new!() 696 | Set.put!(set, {:a, :b, :c}) 697 | 698 | assert_raise RuntimeError, 699 | "ETS.Set.update_element!/3 returned {:error, :position_out_of_bounds}", 700 | fn -> 701 | Set.update_element!(set, :a, [{2, :ok}, {4, :invalid}]) 702 | end 703 | end 704 | 705 | test "update_element!/3 cannot update a record's key" do 706 | set = Set.new!() 707 | Set.put!(set, {:a, :b, :c}) 708 | 709 | assert_raise RuntimeError, 710 | "ETS.Set.update_element!/3 returned {:error, :cannot_update_key}", 711 | fn -> 712 | Set.update_element!(set, :a, {1, :invalid}) 713 | end 714 | end 715 | 716 | test "update_element!/3 fails if key does not exist" do 717 | set = Set.new!() 718 | Set.put(set, {:a, :b, :c}) 719 | 720 | assert_raise RuntimeError, 721 | "ETS.Set.update_element!/3 returned {:error, :key_not_found}", 722 | fn -> 723 | Set.update_element!(set, :b, {2, :foo}) 724 | end 725 | end 726 | end 727 | 728 | describe "Wrap Existing" do 729 | test "wrap_existing!/1 raises on error" do 730 | assert_raise RuntimeError, 731 | "ETS.Set.wrap_existing!/1 returned {:error, :table_not_found}", 732 | fn -> 733 | Set.wrap_existing!(:not_a_table) 734 | end 735 | end 736 | end 737 | 738 | describe "Give Away give_away!/3" do 739 | test "success" do 740 | recipient_pid = self() 741 | 742 | spawn(fn -> 743 | set = Set.new!() 744 | Set.give_away!(set, recipient_pid) 745 | end) 746 | 747 | assert {:ok, %{set: %Set{}, gift: []}} = Set.accept() 748 | end 749 | 750 | test "timeout" do 751 | assert {:error, :timeout} = Set.accept(10) 752 | end 753 | 754 | test "cannot give to process which already owns table" do 755 | assert_raise RuntimeError, 756 | "ETS.Set.give_away!/3 returned {:error, :recipient_already_owns_table}", 757 | fn -> 758 | set = Set.new!() 759 | Set.give_away!(set, self()) 760 | end 761 | end 762 | 763 | test "cannot give to process which is not alive" do 764 | assert_raise RuntimeError, 765 | "ETS.Set.give_away!/3 returned {:error, :recipient_not_alive}", 766 | fn -> 767 | set = Set.new!() 768 | Set.give_away!(set, TestUtils.dead_pid()) 769 | end 770 | end 771 | 772 | test "cannot give a table belonging to another process" do 773 | sender_pid = self() 774 | 775 | _owner_pid = 776 | spawn_link(fn -> 777 | set = Set.new!() 778 | send(sender_pid, set) 779 | Process.sleep(:infinity) 780 | end) 781 | 782 | assert_receive set 783 | 784 | recipient_pid = spawn_link(fn -> Process.sleep(:infinity) end) 785 | 786 | assert_raise RuntimeError, 787 | "ETS.Set.give_away!/3 returned {:error, :sender_not_table_owner}", 788 | fn -> 789 | Set.give_away!(set, recipient_pid) 790 | end 791 | end 792 | end 793 | 794 | describe "Macro" do 795 | test "accept/5 success" do 796 | {:ok, recipient_pid} = start_supervised(ETS.TestServer) 797 | 798 | %Set{table: table} = set = Set.new!() 799 | 800 | Set.give_away!(set, recipient_pid, :set_test) 801 | 802 | assert_receive {:thank_you, %Set{table: ^table}} 803 | end 804 | end 805 | 806 | def table_name, do: String.to_atom("table#{:rand.uniform(9_999_999)}") 807 | 808 | def table_info(%Set{table: table}), do: table_info(table) 809 | 810 | def table_info(id) do 811 | id 812 | |> :ets.info() 813 | |> Enum.into(%{}) 814 | end 815 | end 816 | -------------------------------------------------------------------------------- /test/ets_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ETSTest do 2 | use ExUnit.Case 3 | doctest ETS 4 | 5 | describe "all" do 6 | test "all!/0 returns tables successfully" do 7 | all = :ets.all() 8 | all2 = ETS.all!() 9 | assert length(all) == length(all2) 10 | assert Enum.count(all, &type_old(&1, :set)) == Enum.count(all2, &type_new(&1, :set)) 11 | 12 | assert Enum.count(all, &type_old(&1, :ordered_set)) == 13 | Enum.count(all2, &type_new(&1, :ordered_set)) 14 | 15 | assert Enum.count(all, &type_old(&1, :bag)) == Enum.count(all2, &type_new(&1, :bag)) 16 | 17 | assert Enum.count(all, &type_old(&1, :duplicate_bag)) == 18 | Enum.count(all2, &type_new(&1, :duplicate_bag)) 19 | end 20 | end 21 | 22 | def type_old(tid, type), do: tid |> :ets.info() |> Keyword.get(:type) |> Kernel.==(type) 23 | def type_new(%{info: info}, type), do: info |> Keyword.get(:type) |> Kernel.==(type) 24 | end 25 | -------------------------------------------------------------------------------- /test/support/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.TestServer do 2 | @moduledoc """ 3 | A test process for receiving ETS table ownership messages. 4 | """ 5 | use GenServer 6 | 7 | alias ETS.Bag 8 | alias ETS.KeyValueSet 9 | alias ETS.Set 10 | 11 | require ETS.Bag 12 | require ETS.KeyValueSet 13 | require ETS.Set 14 | 15 | def start_link(_) do 16 | GenServer.start_link(__MODULE__, :init_state) 17 | end 18 | 19 | def init(init_arg) do 20 | {:ok, init_arg} 21 | end 22 | 23 | Set.accept :set_test, set, from, :init_state do 24 | send(from, {:thank_you, set}) 25 | send(self(), {:check_state, Set}) 26 | {:noreply, set} 27 | end 28 | 29 | KeyValueSet.accept :kv_test, kv_set, from, :init_state do 30 | send(from, {:thank_you, kv_set}) 31 | send(self(), {:check_state, KeyValueSet}) 32 | {:noreply, kv_set} 33 | end 34 | 35 | Bag.accept :bag_test, bag, from, :init_state do 36 | send(from, {:thank_you, bag}) 37 | send(self(), {:check_state, Bag}) 38 | {:noreply, bag} 39 | end 40 | 41 | def handle_info({:check_state, type}, state) do 42 | %^type{} = state 43 | {:noreply, state} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/test_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ETS.TestUtils do 2 | @moduledoc """ 3 | Helper functions for testing. 4 | """ 5 | 6 | @doc """ 7 | Returns the pid of a local process which is guaranteed to be dead. 8 | """ 9 | def dead_pid do 10 | pid = spawn(fn -> :ok end) 11 | wait_until_dead(pid) 12 | end 13 | 14 | def otp25? do 15 | {version, ""} = :erlang.system_info(:otp_release) |> to_string() |> Integer.parse() 16 | version >= 25 17 | end 18 | 19 | defp wait_until_dead(pid) do 20 | if Process.alive?(pid) do 21 | wait_until_dead(pid) 22 | else 23 | pid 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------